diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 581f26d896..e79a4de7cc 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -13573,6 +13573,7 @@ Sorry for the inconvenience."; "FolderLinkPreview.ToastFolderAddedTitleV2" = "Folder {folder} Added"; "Stars.Purchase.UpgradeStarGiftInfo" = "Buy Stars to upgrade your gift into a unique collectible."; +"Stars.Purchase.TransferStarGiftInfo" = "Buy Stars to transfer ownership of your unique collectible."; "Gift.Send.Upgrade" = "Make Unique for %@"; "Gift.Send.Upgrade.Info" = "Enable this to let %1$@ turn your gift into a unique collectible. [Learn More >]()"; @@ -14002,6 +14003,16 @@ Sorry for the inconvenience."; "Share.VideoStartAt" = "Start at %@"; "SendStarReactions.SubtitleFrom" = "from %@"; +"Notification.PaidMessageRefund.Stars_1" = "%@ Star"; +"Notification.PaidMessageRefund.Stars_any" = "%@ Stars"; +"Notification.PaidMessageRefund" = "%1$@ refunded you %2$@"; +"Notification.PaidMessageRefundYou" = "You refunded %1$@ to %2$@"; + +"Notification.PaidMessagePriceChanged.Stars_1" = "%@ Star"; +"Notification.PaidMessagePriceChanged.Stars_any" = "%@ Stars"; +"Notification.PaidMessagePriceChanged" = "%1$@ changed price to %2$@ per message"; +"Notification.PaidMessagePriceChangedYou" = "You changed price to %1$@ per message"; + "Premium.MaxExpiringStoriesTextNumberFormat_1" = "**%d** story"; "Premium.MaxExpiringStoriesTextNumberFormat_any" = "**%d** stories"; "Premium.MaxExpiringStoriesTextPremiumNumberFormat_1" = "**%d** story"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 9cd23dc708..958b1beef7 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1053,6 +1053,8 @@ public protocol SharedAccountContext: AnyObject { func makeChatQrCodeScreen(context: AccountContext, peer: Peer, threadId: Int64?, temporary: Bool) -> ViewController func makePremiumIntroController(context: AccountContext, source: PremiumIntroSource, forceDark: Bool, dismissed: (() -> Void)?) -> ViewController + func makePremiumIntroController(sharedContext: SharedAccountContext, engine: TelegramEngineUnauthorized, inAppPurchaseManager: InAppPurchaseManager, source: PremiumIntroSource, proceed: (() -> Void)?) -> ViewController + func makePremiumDemoController(context: AccountContext, subject: PremiumDemoSubject, forceDark: Bool, action: @escaping () -> Void, dismissed: (() -> Void)?) -> ViewController func makePremiumLimitController(context: AccountContext, subject: PremiumLimitSubject, count: Int32, forceDark: Bool, cancel: @escaping () -> Void, action: @escaping () -> Bool) -> ViewController @@ -1129,6 +1131,8 @@ public protocol SharedAccountContext: AnyObject { func makeGalleryController(context: AccountContext, source: GalleryControllerItemSource, streamSingleVideo: Bool, isPreview: Bool) -> ViewController + func makeAccountFreezeInfoScreen(context: AccountContext) -> ViewController + func makeDebugSettingsController(context: AccountContext?) -> ViewController? func navigateToCurrentCall() @@ -1228,6 +1232,7 @@ public protocol AccountContext: AnyObject { var availableMessageEffects: Signal { get } var isPremium: Bool { get } + var isFrozen: Bool { get } var userLimits: EngineConfiguration.UserLimits { get } var peerNameColors: PeerNameColors { get } diff --git a/submodules/AccountContext/Sources/Premium.swift b/submodules/AccountContext/Sources/Premium.swift index 15277eccbc..790a21b077 100644 --- a/submodules/AccountContext/Sources/Premium.swift +++ b/submodules/AccountContext/Sources/Premium.swift @@ -43,6 +43,7 @@ public enum PremiumIntroSource { case animatedEmoji case messageEffects case paidMessages + case auth(String) } public enum PremiumGiftSource: Equatable { @@ -136,6 +137,7 @@ public enum StarsPurchasePurpose: Equatable { case unlockMedia(requiredStars: Int64) case starGift(peerId: EnginePeer.Id, requiredStars: Int64) case upgradeStarGift(requiredStars: Int64) + case transferStarGift(requiredStars: Int64) case sendMessage(peerId: EnginePeer.Id, requiredStars: Int64) } @@ -294,6 +296,44 @@ public struct PremiumConfiguration { } } +public struct AccountFreezeConfiguration { + public static var defaultValue: AccountFreezeConfiguration { + return AccountFreezeConfiguration( + freezeSinceDate: nil, + freezeUntilDate: nil, + freezeAppealUrl: nil + ) + } + + public let freezeSinceDate: Int32? + public let freezeUntilDate: Int32? + public let freezeAppealUrl: String? + + fileprivate init( + freezeSinceDate: Int32?, + freezeUntilDate: Int32?, + freezeAppealUrl: String? + ) { + self.freezeSinceDate = freezeSinceDate + self.freezeUntilDate = freezeUntilDate + self.freezeAppealUrl = freezeAppealUrl + } + + public static func with(appConfiguration: AppConfiguration) -> AccountFreezeConfiguration { + let defaultValue = self.defaultValue + if let data = appConfiguration.data { + return AccountFreezeConfiguration( + freezeSinceDate: (data["freeze_since_date"] as? Double).flatMap(Int32.init) ?? defaultValue.freezeSinceDate, + freezeUntilDate: (data["freeze_until_date"] as? Double).flatMap(Int32.init) ?? defaultValue.freezeUntilDate, + freezeAppealUrl: data["freeze_appeal_url"] as? String ?? defaultValue.freezeAppealUrl + ) + } else { + return defaultValue + } + } +} + + public protocol GiftOptionsScreenProtocol { } diff --git a/submodules/AuthorizationUI/BUILD b/submodules/AuthorizationUI/BUILD index f941572f4b..a0657ad3e9 100644 --- a/submodules/AuthorizationUI/BUILD +++ b/submodules/AuthorizationUI/BUILD @@ -42,6 +42,8 @@ swift_library( "//submodules/TelegramUI/Components/TextNodeWithEntities:TextNodeWithEntities", "//submodules/MoreButtonNode:MoreButtonNode", "//submodules/ContextUI:ContextUI", + "//submodules/InAppPurchaseManager", + "//submodules/TelegramUI/Components/Premium/PremiumCoinComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift index 293e7475b9..0ddd8a45c6 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift @@ -20,6 +20,7 @@ import TelegramNotices import AuthenticationServices import Markdown import AlertUI +import InAppPurchaseManager import ObjectiveC private var ObjCKey_Delegate: Int? @@ -59,6 +60,8 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth return TelegramEngineUnauthorized(account: self.account) } + private var inAppPurchaseManager: InAppPurchaseManager! + public init(sharedContext: SharedAccountContext, account: UnauthorizedAccount, otherAccountPhoneNumbers: ((String, AccountRecordId, Bool)?, [(String, AccountRecordId, Bool)]), presentationData: PresentationData, openUrl: @escaping (String) -> Void, apiId: Int32, apiHash: String, authorizationCompleted: @escaping () -> Void) { self.sharedContext = sharedContext self.account = account @@ -79,6 +82,8 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth super.init(mode: .single, theme: NavigationControllerTheme(statusBar: navigationStatusBar, navigationBar: AuthorizationSequenceController.navigationBarTheme(presentationData.theme), emptyAreaColor: .black), isFlat: true) + self.inAppPurchaseManager = InAppPurchaseManager(engine: .unauthorized(self.engine)) + self.stateDisposable = (self.engine.auth.state() |> map { state -> InnerState in if case .authorized = state { @@ -758,6 +763,17 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth return controller } + private func paymentController(number: String, phoneCodeHash: String, storeProduct: String) -> AuthorizationSequencePaymentScreen { + let controller = AuthorizationSequencePaymentScreen(sharedContext: self.sharedContext, engine: self.engine, presentationData: self.presentationData, inAppPurchaseManager: self.inAppPurchaseManager, phoneNumber: number, phoneCodeHash: phoneCodeHash, storeProduct: storeProduct, back: { [weak self] in + guard let self else { + return + } + let countryCode = AuthorizationSequenceController.defaultCountryCode() + let _ = self.engine.auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: self.account.testingEnvironment, masterDatacenterId: self.account.masterDatacenterId, contents: .phoneEntry(countryCode: countryCode, number: ""))).startStandalone() + }) + return controller + } + @available(iOS 13.0, *) public func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { let lastController = self.viewControllers.last as? ViewController @@ -1285,6 +1301,13 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth } controllers.append(self.signUpController(firstName: firstName, lastName: lastName, termsOfService: termsOfService, displayCancel: displayCancel)) self.setViewControllers(controllers, animated: !self.viewControllers.isEmpty) + case let .payment(number, codeHash, storeProduct, _): + var controllers: [ViewController] = [] + if !self.otherAccountPhoneNumbers.1.isEmpty { + controllers.append(self.splashController()) + } + controllers.append(self.paymentController(number: number, phoneCodeHash: codeHash, storeProduct: storeProduct)) + self.setViewControllers(controllers, animated: !self.viewControllers.isEmpty) } } } diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequencePaymentScreen.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequencePaymentScreen.swift new file mode 100644 index 0000000000..51354a786d --- /dev/null +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequencePaymentScreen.swift @@ -0,0 +1,602 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import Postbox +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import TelegramStringFormatting +import PresentationDataUtils +import ComponentFlow +import ViewControllerComponent +import MultilineTextComponent +import BalancedTextComponent +import BundleIconComponent +import ButtonComponent +import TextFormat +import InAppPurchaseManager +import ConfettiEffect +import PremiumCoinComponent +import Markdown +import CountrySelectionUI +import AccountContext + +final class AuthorizationSequencePaymentScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let sharedContext: SharedAccountContext + let engine: TelegramEngineUnauthorized + let inAppPurchaseManager: InAppPurchaseManager + let presentationData: PresentationData + let phoneNumber: String + let phoneCodeHash: String + let storeProduct: String + + init( + sharedContext: SharedAccountContext, + engine: TelegramEngineUnauthorized, + inAppPurchaseManager: InAppPurchaseManager, + presentationData: PresentationData, + phoneNumber: String, + phoneCodeHash: String, + storeProduct: String + ) { + self.sharedContext = sharedContext + self.engine = engine + self.inAppPurchaseManager = inAppPurchaseManager + self.presentationData = presentationData + self.phoneNumber = phoneNumber + self.phoneCodeHash = phoneCodeHash + self.storeProduct = storeProduct + } + + static func ==(lhs: AuthorizationSequencePaymentScreenComponent, rhs: AuthorizationSequencePaymentScreenComponent) -> Bool { + if lhs.storeProduct != rhs.storeProduct { + return false + } + return true + } + + final class View: UIView { + private let animation = ComponentView() + private let title = ComponentView() + private let list = ComponentView() + private let check = ComponentView() + private let button = ComponentView() + + private var isUpdating: Bool = false + + private var component: AuthorizationSequencePaymentScreenComponent? + private(set) weak var state: EmptyComponentState? + private var environment: EnvironmentType? + + private var products: [InAppPurchaseManager.Product] = [] + private var productsDisposable: Disposable? + private var inProgress = false + + override init(frame: CGRect) { + super.init(frame: frame) + + self.disablesInteractiveKeyboardGestureRecognizer = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.productsDisposable?.dispose() + } + + private func proceed() { + guard let component = self.component, let storeProduct = self.products.first(where: { $0.id == component.storeProduct }) else { + return + } + + self.inProgress = true + self.state?.updated() + + let (currency, amount) = storeProduct.priceCurrencyAndAmount + let purpose: AppStoreTransactionPurpose = .authCode(restore: false, phoneNumber: component.phoneNumber, phoneCodeHash: component.phoneCodeHash, currency: currency, amount: amount) + let _ = (component.engine.payments.canPurchasePremium(purpose: purpose) + |> deliverOnMainQueue).start(next: { [weak self] available in + guard let self else { + return + } + let presentationData = component.presentationData + if available { + let _ = (component.inAppPurchaseManager.buyProduct(storeProduct, quantity: 1, purpose: purpose) + |> deliverOnMainQueue).start(next: { [weak self] status in + let _ = status + let _ = self + }, error: { [weak self] error in + guard let self, let controller = self.environment?.controller() else { + return + } + self.inProgress = false + self.state?.updated(transition: .immediate) + + var errorText: String? + switch error { + case .generic: + errorText = presentationData.strings.Premium_Purchase_ErrorUnknown + case .network: + errorText = presentationData.strings.Premium_Purchase_ErrorNetwork + case .notAllowed: + errorText = presentationData.strings.Premium_Purchase_ErrorNotAllowed + case .cantMakePayments: + errorText = presentationData.strings.Premium_Purchase_ErrorCantMakePayments + case .assignFailed: + errorText = presentationData.strings.Premium_Purchase_ErrorUnknown + case .tryLater: + errorText = presentationData.strings.Premium_Purchase_ErrorUnknown + case .cancelled: + break + } + + if let errorText { + //addAppLogEvent(postbox: component.engine.account.postbox, type: "premium_gift.promo_screen_fail") + + let _ = errorText + let _ = controller + //let alertController = textAlertController(context: component.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) + //controller.present(alertController, in: .window(.root)) + } + }) + } else { + self.inProgress = false + self.state?.updated(transition: .immediate) + } + }) + } + + func update(component: AuthorizationSequencePaymentScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + let environment = environment[EnvironmentType.self].value + let themeUpdated = self.environment?.theme !== environment.theme + self.environment = environment + self.state = state + + if self.component == nil { + self.productsDisposable = (component.inAppPurchaseManager.availableProducts + |> deliverOnMainQueue).start(next: { [weak self] products in + guard let self else { + return + } + self.products = products + if !self.isUpdating { + self.state?.updated() + } + }) + } + + self.component = component + + if themeUpdated { + self.backgroundColor = environment.theme.list.plainBackgroundColor + } + + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + + let animationSize = self.animation.update( + transition: transition, + component: AnyComponent(PremiumCoinComponent( + mode: .business, + isIntro: true, + isVisible: true, + hasIdleAnimations: true + )), + environment: {}, + containerSize: CGSize(width: min(414.0, availableSize.width), height: 184.0) + ) + let titleSize = self.title.update( + transition: transition, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: "SMS Fee", font: Font.bold(28.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor))) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) + ) + + let textColor = environment.theme.list.itemPrimaryTextColor + let secondaryTextColor = environment.theme.list.itemSecondaryTextColor + let linkColor = environment.theme.list.itemAccentColor + + var countryName: String = "" + if let (country, _) = AuthorizationSequenceCountrySelectionController.lookupCountryIdByNumber(component.phoneNumber, preferredCountries: [:]) { + countryName = AuthorizationSequenceCountrySelectionController.lookupCountryNameById(country.id, strings: environment.strings) ?? country.name + } + + var items: [AnyComponentWithIdentity] = [] + items.append( + AnyComponentWithIdentity( + id: "cost", + component: AnyComponent(ParagraphComponent( + title: "High SMS Costs", + titleColor: textColor, + text: "Telecom providers in your country (\(countryName)) charge Telegram very high prices for SMS.", + textColor: secondaryTextColor, + iconName: "Premium/Authorization/Cost", + iconColor: linkColor + )) + ) + ) + items.append( + AnyComponentWithIdentity( + id: "verification", + component: AnyComponent(ParagraphComponent( + title: "Verification Required", + titleColor: textColor, + text: "Telegram needs to send you an SMS with a verification code to confirm your phone number.", + textColor: secondaryTextColor, + iconName: "Premium/Authorization/Verification", + iconColor: linkColor + )) + ) + ) + items.append( + AnyComponentWithIdentity( + id: "withdrawal", + component: AnyComponent(ParagraphComponent( + title: "Support via [Telegram Premium >]()", + titleColor: textColor, + text: "Sign up for a 1-week Telegram Premium subscription to help cover the SMS costs.", + textColor: secondaryTextColor, + iconName: "Premium/Authorization/Support", + iconColor: linkColor, + action: { [weak self] in + guard let self, let controller = self.environment?.controller(), let product = self.products.first(where: { $0.id == component.storeProduct }) else { + return + } + let introController = component.sharedContext.makePremiumIntroController( + sharedContext: component.sharedContext, + engine: component.engine, + inAppPurchaseManager: component.inAppPurchaseManager, + source: .auth(product.price), + proceed: { [weak self] in + self?.proceed() + } + ) + controller.push(introController) + } + )) + ) + ) + + let listSize = self.list.update( + transition: transition, + component: AnyComponent(List(items)), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height) + ) + + let buttonHeight: CGFloat = 50.0 + let bottomPanelPadding: CGFloat = 12.0 + let titleSpacing: CGFloat = -24.0 + let listSpacing: CGFloat = 12.0 + let totalHeight = animationSize.height + titleSpacing + titleSize.height + listSpacing + listSize.height + + var originY = floor((availableSize.height - buttonHeight - bottomPanelPadding * 2.0 - totalHeight) / 2.0) + + if let animationView = self.animation.view { + if animationView.superview == nil { + self.addSubview(animationView) + } + animationView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - animationSize.width) / 2.0), y: originY), size: animationSize) + originY += animationSize.height + titleSpacing + } + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + titleView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) / 2.0), y: originY), size: titleSize) + originY += titleSize.height + listSpacing + } + + if let listView = self.list.view { + if listView.superview == nil { + self.addSubview(listView) + } + listView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - listSize.width) / 2.0), y: originY), size: listSize) + } + + let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding + let bottomPanelHeight = bottomPanelPadding + buttonHeight + bottomInset + + let priceString: String + if let product = self.products.first(where: { $0.id == component.storeProduct }) { + priceString = product.price + } else { + priceString = "–" + } + let buttonString = "Sign up for \(priceString)" + let buttonAttributedString = NSMutableAttributedString(string: buttonString, font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) + 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.9), + cornerRadius: 10.0 + ), + content: AnyComponentWithIdentity( + id: AnyHashable(buttonString), + component: AnyComponent( + VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString)))), + AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: "Get Telegram Premium for 1 week", font: Font.medium(11.0), textColor: environment.theme.list.itemCheckColors.foregroundColor.withAlphaComponent(0.7), paragraphAlignment: .center))))) + ], spacing: 1.0) + ) + ), + isEnabled: true, + displaysProgress: self.inProgress, + action: { [weak self] in + self?.proceed() + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: buttonHeight) + ) + if let buttonView = self.button.view { + if buttonView.superview == nil { + self.addSubview(buttonView) + } + buttonView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - buttonSize.width) / 2.0), y: availableSize.height - bottomPanelHeight + bottomPanelPadding), size: buttonSize) + } + + return availableSize + } + } + + func makeView() -> View { + return View() + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +public final class AuthorizationSequencePaymentScreen: ViewControllerComponentContainer { + public init( + sharedContext: SharedAccountContext, + engine: TelegramEngineUnauthorized, + presentationData: PresentationData, + inAppPurchaseManager: InAppPurchaseManager, + phoneNumber: String, + phoneCodeHash: String, + storeProduct: String, + back: @escaping () -> Void + ) { + super.init(component: AuthorizationSequencePaymentScreenComponent( + sharedContext: sharedContext, + engine: engine, + inAppPurchaseManager: inAppPurchaseManager, + presentationData: presentationData, + phoneNumber: phoneNumber, + phoneCodeHash: phoneCodeHash, + storeProduct: storeProduct + ), navigationBarAppearance: .transparent, theme: .default, updatedPresentationData: (initial: presentationData, signal: .single(presentationData))) + + loadServerCountryCodes(accountManager: sharedContext.accountManager, engine: engine, completion: { [weak self] in + if let strongSelf = self { + strongSelf.requestLayout(forceUpdate: true, transition: .immediate) + } + }) + + self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) + + + self.attemptNavigation = { _ in + return false + } + self.navigationBar?.backPressed = { + back() + } + } + + public override func loadDisplayNode() { + super.loadDisplayNode() + + self.displayNode.view.disableAutomaticKeyboardHandling = [.forward, .backward] + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + } + + @objc private func cancelPressed() { + self.dismiss() + } +} + +private final class ParagraphComponent: CombinedComponent { + let title: String + let titleColor: UIColor + let text: String + let textColor: UIColor + let iconName: String + let iconColor: UIColor + let action: (() -> Void)? + + public init( + title: String, + titleColor: UIColor, + text: String, + textColor: UIColor, + iconName: String, + iconColor: UIColor, + action: (() -> Void)? = nil + ) { + self.title = title + self.titleColor = titleColor + self.text = text + self.textColor = textColor + self.iconName = iconName + self.iconColor = iconColor + self.action = action + } + + 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 + } + + final class State: ComponentState { + var cachedChevronImage: (UIImage, UIColor)? + } + + func makeState() -> State { + return State() + } + + 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 state = context.state + + 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 textFont = Font.regular(15.0) + let boldTextFont = Font.semibold(15.0) + let titleColor = component.titleColor + let textColor = component.textColor + let linkColor = component.iconColor + let titleMarkdownAttributes = MarkdownAttributes( + body: MarkdownAttributeSet(font: boldTextFont, textColor: titleColor), + bold: MarkdownAttributeSet(font: boldTextFont, textColor: titleColor), + link: MarkdownAttributeSet(font: boldTextFont, textColor: linkColor), + linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + } + ) + + if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== linkColor { + state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: linkColor)!, linkColor) + } + + let titleAttributedString = parseMarkdownIntoAttributedString(component.title, attributes: titleMarkdownAttributes).mutableCopy() as! NSMutableAttributedString + if let range = titleAttributedString.string.range(of: ">"), let chevronImage = state.cachedChevronImage?.0 { + titleAttributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: titleAttributedString.string)) + } + + let title = title.update( + component: MultilineTextComponent( + text: .plain(titleAttributedString), + horizontalAlignment: .center, + maximumNumberOfLines: 1, + highlightColor: linkColor.withAlphaComponent(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + }, + tapAction: { attributes, _ in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { + component.action?() + } + } + ), + availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + + + let textMarkdownAttributes = MarkdownAttributes( + body: MarkdownAttributeSet(font: textFont, textColor: textColor), + bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), + link: MarkdownAttributeSet(font: textFont, textColor: linkColor), + linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + } + ) + + let text = text.update( + component: MultilineTextComponent( + text: .markdown(text: component.text, attributes: textMarkdownAttributes), + horizontalAlignment: .natural, + maximumNumberOfLines: 0, + lineSpacing: 0.2, + highlightColor: linkColor.withAlphaComponent(0.1), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + }, + tapAction: { attributes, _ in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { + component.action?() + } + } + ), + 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 + 18.0) + } + } +} diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 9a07996286..6d5338baa8 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -1208,6 +1208,14 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController ) } + self.chatListDisplayNode.mainContainerNode.openAccountFreezeInfo = { [weak self] in + guard let self else { + return + } + let controller = self.context.sharedContext.makeAccountFreezeInfoScreen(context: self.context) + self.push(controller) + } + self.chatListDisplayNode.mainContainerNode.openPhotoSetup = { [weak self] in guard let self else { return @@ -2786,6 +2794,12 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController private weak var storyCameraTooltip: TooltipScreen? fileprivate func openStoryCamera(fromList: Bool) { + guard !self.context.isFrozen else { + let controller = self.context.sharedContext.makeAccountFreezeInfoScreen(context: self.context) + self.push(controller) + return + } + var reachedCountLimit = false var premiumNeeded = false var hasActiveCall = false @@ -4633,6 +4647,12 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } @objc fileprivate func composePressed() { + guard !self.context.isFrozen else { + let controller = self.context.sharedContext.makeAccountFreezeInfoScreen(context: self.context) + self.push(controller) + return + } + guard let navigationController = self.navigationController as? NavigationController else { return } diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index 6f71a800bd..ef7d0214de 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -357,6 +357,9 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele itemNode.listNode.openPhotoSetup = { [weak self] in self?.openPhotoSetup?() } + itemNode.listNode.openAccountFreezeInfo = { [weak self] in + self?.openAccountFreezeInfo?() + } self.currentItemStateValue.set(itemNode.listNode.state |> map { state in let filterId: Int32? @@ -425,6 +428,7 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele var openStarsTopup: ((Int64?) -> Void)? var openWebApp: ((TelegramUser) -> Void)? var openPhotoSetup: (() -> Void)? + var openAccountFreezeInfo: (() -> Void)? var addedVisibleChatsWithPeerIds: (([EnginePeer.Id]) -> Void)? var didBeginSelectingChats: (() -> Void)? var canExpandHiddenItems: (() -> Bool)? diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 428db40881..d2e047c8be 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -3281,6 +3281,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { }, openPhotoSetup: { }, openAdInfo: { node in interaction.openAdInfo(node) + }, openAccountFreezeInfo: { }) chatListInteraction.isSearchMode = true @@ -5263,6 +5264,7 @@ public final class ChatListSearchShimmerNode: ASDisplayNode { }, openWebApp: { _ in }, openPhotoSetup: { }, openAdInfo: { _ in + }, openAccountFreezeInfo: { }) var isInlineMode = false if case .topics = key { diff --git a/submodules/ChatListUI/Sources/ChatListShimmerNode.swift b/submodules/ChatListUI/Sources/ChatListShimmerNode.swift index 1f5727d75f..cc9dcbf111 100644 --- a/submodules/ChatListUI/Sources/ChatListShimmerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListShimmerNode.swift @@ -162,6 +162,7 @@ public final class ChatListShimmerNode: ASDisplayNode { }, openWebApp: { _ in }, openPhotoSetup: { }, openAdInfo: { _ in + }, openAccountFreezeInfo: { }) interaction.isInlineMode = isInlineMode diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 86d3f3b783..e379965e2d 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -4102,7 +4102,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { strongSelf.secretIconNode = iconNode } iconNode.image = currentSecretIconImage - transition.updateFrame(node: iconNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x, y: contentRect.origin.y + floor((titleLayout.size.height - currentSecretIconImage.size.height) / 2.0)), size: currentSecretIconImage.size)) + transition.updateFrame(node: iconNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x + titleLeftOffset, y: contentRect.origin.y + floor((titleLayout.size.height - currentSecretIconImage.size.height) / 2.0)), size: currentSecretIconImage.size)) titleOffset += currentSecretIconImage.size.width + 3.0 } else if let secretIconNode = strongSelf.secretIconNode { strongSelf.secretIconNode = nil diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index 0e83bccb9e..8175610721 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -115,6 +115,7 @@ public final class ChatListNodeInteraction { let openWebApp: (TelegramUser) -> Void let openPhotoSetup: () -> Void let openAdInfo: (ASDisplayNode) -> Void + let openAccountFreezeInfo: () -> Void public var searchTextHighightState: String? var highlightedChatLocation: ChatListHighlightedLocation? @@ -173,7 +174,8 @@ public final class ChatListNodeInteraction { editPeer: @escaping (ChatListItem) -> Void, openWebApp: @escaping (TelegramUser) -> Void, openPhotoSetup: @escaping () -> Void, - openAdInfo: @escaping (ASDisplayNode) -> Void + openAdInfo: @escaping (ASDisplayNode) -> Void, + openAccountFreezeInfo: @escaping () -> Void ) { self.activateSearch = activateSearch self.peerSelected = peerSelected @@ -220,6 +222,7 @@ public final class ChatListNodeInteraction { self.openWebApp = openWebApp self.openPhotoSetup = openPhotoSetup self.openAdInfo = openAdInfo + self.openAccountFreezeInfo = openAccountFreezeInfo } } @@ -770,6 +773,8 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL nodeInteraction?.openStarsTopup(amount.value) case .setupPhoto: nodeInteraction?.openPhotoSetup() + case .accountFreeze: + nodeInteraction?.openAccountFreezeInfo() } case .hide: nodeInteraction?.dismissNotice(notice) @@ -1116,6 +1121,8 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL nodeInteraction?.openStarsTopup(amount.value) case .setupPhoto: nodeInteraction?.openPhotoSetup() + case .accountFreeze: + nodeInteraction?.openAccountFreezeInfo() } case .hide: nodeInteraction?.dismissNotice(notice) @@ -1239,6 +1246,7 @@ public final class ChatListNode: ListView { public var openWebApp: ((TelegramUser) -> Void)? public var openPhotoSetup: (() -> Void)? public var openAdInfo: ((ASDisplayNode) -> Void)? + public var openAccountFreezeInfo: (() -> Void)? private var theme: PresentationTheme @@ -1899,6 +1907,8 @@ public final class ChatListNode: ListView { self.openPhotoSetup?() }, openAdInfo: { [weak self] node in self?.openAdInfo?(node) + }, openAccountFreezeInfo: { [weak self] in + self?.openAccountFreezeInfo?() }) nodeInteraction.isInlineMode = isInlineMode @@ -1988,6 +1998,16 @@ public final class ChatListNode: ListView { let twoStepData: Signal = .single(nil) |> then(context.engine.auth.twoStepVerificationConfiguration() |> map(Optional.init)) + let accountFreezeConfiguration = (context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]) + |> map { view -> AppConfiguration in + let appConfiguration: AppConfiguration = view.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue + return appConfiguration + } + |> distinctUntilChanged + |> map { appConfiguration -> AccountFreezeConfiguration in + return AccountFreezeConfiguration.with(appConfiguration: appConfiguration) + }) + let suggestedChatListNoticeSignal: Signal = combineLatest( context.engine.notices.getServerProvidedSuggestions(), context.engine.notices.getServerDismissedSuggestions(), @@ -1998,11 +2018,12 @@ public final class ChatListNode: ListView { TelegramEngine.EngineData.Item.Peer.Birthday(id: context.account.peerId) ), context.account.stateManager.contactBirthdays, - starsSubscriptionsContextPromise.get() + starsSubscriptionsContextPromise.get(), + accountFreezeConfiguration ) - |> mapToSignal { suggestions, dismissedSuggestions, configuration, newSessionReviews, data, birthdays, starsSubscriptionsContext -> Signal in + |> mapToSignal { suggestions, dismissedSuggestions, configuration, newSessionReviews, data, birthdays, starsSubscriptionsContext, accountFreezeConfiguration -> Signal in let (accountPeer, birthday) = data - + if let newSessionReview = newSessionReviews.first { return .single(.reviewLogin(newSessionReview: newSessionReview, totalCount: newSessionReviews.count)) } @@ -2035,8 +2056,10 @@ public final class ChatListNode: ListView { if dismissedSuggestions.contains(.todayBirthdays) { todayBirthdayPeerIds = [] } - - if suggestions.contains(.starsSubscriptionLowBalance) { + + if let _ = accountFreezeConfiguration.freezeUntilDate { + return .single(.accountFreeze) + } else if suggestions.contains(.starsSubscriptionLowBalance) { if let starsSubscriptionsContext { return starsSubscriptionsContext.state |> map { state in diff --git a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift index 4a338533a2..9a839cdb9c 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift @@ -92,6 +92,7 @@ public enum ChatListNotice: Equatable { case premiumGrace case starsSubscriptionLowBalance(amount: StarsAmount, peers: [EnginePeer]) case setupPhoto(EnginePeer) + case accountFreeze } enum ChatListNodeEntry: Comparable, Identifiable { diff --git a/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift b/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift index e1a1f44eba..14f4e82614 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift @@ -288,6 +288,10 @@ final class ChatListNoticeItemNode: ItemListRevealOptionsItemNode { titleString = NSAttributedString(string: item.strings.ChatList_AddPhoto_Title, font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor) textString = NSAttributedString(string: item.strings.ChatList_AddPhoto_Text, font: smallTextFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor) avatarPeer = accountPeer + case .accountFreeze: + //TODO:localize + titleString = NSAttributedString(string: "Your account is frozen", font: titleFont, textColor: item.theme.list.itemDestructiveColor) + textString = NSAttributedString(string: "Tap to view details and submit an appeal.", font: smallTextFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor) } var leftInset: CGFloat = sideInset diff --git a/submodules/Components/SolidRoundedButtonComponent/Sources/SolidRoundedButtonComponent.swift b/submodules/Components/SolidRoundedButtonComponent/Sources/SolidRoundedButtonComponent.swift index aa65028e56..f6630c9ac9 100644 --- a/submodules/Components/SolidRoundedButtonComponent/Sources/SolidRoundedButtonComponent.swift +++ b/submodules/Components/SolidRoundedButtonComponent/Sources/SolidRoundedButtonComponent.swift @@ -9,6 +9,7 @@ public final class SolidRoundedButtonComponent: Component { public typealias Theme = SolidRoundedButtonTheme public let title: String? + public let subtitle: String? public let label: String? public let badge: String? public let icon: UIImage? @@ -28,6 +29,7 @@ public final class SolidRoundedButtonComponent: Component { public init( title: String? = nil, + subtitle: String? = nil, label: String? = nil, badge: String? = nil, icon: UIImage? = nil, @@ -46,6 +48,7 @@ public final class SolidRoundedButtonComponent: Component { action: @escaping () -> Void ) { self.title = title + self.subtitle = subtitle self.label = label self.badge = badge self.icon = icon @@ -68,6 +71,9 @@ public final class SolidRoundedButtonComponent: Component { if lhs.title != rhs.title { return false } + if lhs.subtitle != rhs.subtitle { + return false + } if lhs.label != rhs.label { return false } @@ -147,6 +153,7 @@ public final class SolidRoundedButtonComponent: Component { if let button = self.button { button.title = component.title + button.subtitle = component.subtitle button.label = component.label button.badge = component.badge button.iconPosition = component.iconPosition diff --git a/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift b/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift index b8955192e2..4eeac4b0c1 100644 --- a/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift +++ b/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift @@ -151,8 +151,8 @@ open class ViewControllerComponentContainer: ViewController { private var currentIsVisible: Bool = false private var currentLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)? - init(context: AccountContext, controller: ViewControllerComponentContainer, component: AnyComponent, theme: Theme) { - self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + init(presentationData: PresentationData, controller: ViewControllerComponentContainer, component: AnyComponent, theme: Theme) { + self.presentationData = presentationData self.controller = controller @@ -234,7 +234,7 @@ open class ViewControllerComponentContainer: ViewController { return self.displayNode as! Node } - private let context: AccountContext + private var presentationData: PresentationData private var theme: Theme public private(set) var component: AnyComponent @@ -242,6 +242,7 @@ open class ViewControllerComponentContainer: ViewController { public private(set) var validLayout: ContainerViewLayout? public var wasDismissed: (() -> Void)? + public var customProceed: (() -> Void)? public init( context: AccountContext, @@ -252,17 +253,19 @@ open class ViewControllerComponentContainer: ViewController { theme: Theme = .default, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil ) where C.EnvironmentType == ViewControllerComponentContainer.Environment { - self.context = context self.component = AnyComponent(component) self.theme = theme - let presentationData: PresentationData + var effectiveUpdatedPresentationData: (initial: PresentationData, signal: Signal) if let updatedPresentationData { - presentationData = updatedPresentationData.initial + effectiveUpdatedPresentationData = updatedPresentationData } else { - presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + effectiveUpdatedPresentationData = (initial: context.sharedContext.currentPresentationData.with { $0 }, signal: context.sharedContext.presentationData) } + let presentationData = effectiveUpdatedPresentationData.initial + self.presentationData = presentationData + let navigationBarPresentationData: NavigationBarPresentationData? switch navigationBarAppearance { case .none: @@ -274,7 +277,47 @@ open class ViewControllerComponentContainer: ViewController { } super.init(navigationBarPresentationData: navigationBarPresentationData) - self.presentationDataDisposable = ((updatedPresentationData?.signal ?? self.context.sharedContext.presentationData) + self.setupPresentationData(effectiveUpdatedPresentationData, navigationBarAppearance: navigationBarAppearance, statusBarStyle: statusBarStyle, presentationMode: presentationMode) + } + + public init( + component: C, + navigationBarAppearance: NavigationBarAppearance, + statusBarStyle: StatusBarStyle = .default, + presentationMode: PresentationMode = .default, + theme: Theme = .default, + updatedPresentationData: (initial: PresentationData, signal: Signal) + ) where C.EnvironmentType == ViewControllerComponentContainer.Environment { + self.component = AnyComponent(component) + self.theme = theme + + let presentationData = updatedPresentationData.initial + self.presentationData = presentationData + + let navigationBarPresentationData: NavigationBarPresentationData? + switch navigationBarAppearance { + case .none: + navigationBarPresentationData = nil + case .transparent: + navigationBarPresentationData = NavigationBarPresentationData(presentationData: presentationData, hideBackground: true, hideBadge: false, hideSeparator: true) + case .default: + navigationBarPresentationData = NavigationBarPresentationData(presentationData: presentationData) + } + super.init(navigationBarPresentationData: navigationBarPresentationData) + + self.setupPresentationData(updatedPresentationData, navigationBarAppearance: navigationBarAppearance, statusBarStyle: statusBarStyle, presentationMode: presentationMode) + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.presentationDataDisposable?.dispose() + } + + private func setupPresentationData(_ updatedPresentationData: (initial: PresentationData, signal: Signal), navigationBarAppearance: NavigationBarAppearance, statusBarStyle: StatusBarStyle, presentationMode: PresentationMode) { + self.presentationDataDisposable = (updatedPresentationData.signal |> deliverOnMainQueue).start(next: { [weak self] presentationData in if let strongSelf = self { var theme = presentationData.theme @@ -329,16 +372,8 @@ open class ViewControllerComponentContainer: ViewController { } } - required public init(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - self.presentationDataDisposable?.dispose() - } - override open func loadDisplayNode() { - self.displayNode = Node(context: self.context, controller: self, component: self.component, theme: self.theme) + self.displayNode = Node(presentationData: self.presentationData, controller: self, component: self.component, theme: self.theme) self.displayNodeDidLoad() } diff --git a/submodules/ContactListUI/Sources/ContactListNode.swift b/submodules/ContactListUI/Sources/ContactListNode.swift index f33c2c5dba..4e29b6eb61 100644 --- a/submodules/ContactListUI/Sources/ContactListNode.swift +++ b/submodules/ContactListUI/Sources/ContactListNode.swift @@ -591,7 +591,7 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis allSelected = false } var actionTitle: String? - if peerIds.count > 1 { + if !"".isEmpty, peerIds.count > 1 { actionTitle = allSelected ? strings.Premium_Gift_ContactSelection_DeselectAll.uppercased() : strings.Premium_Gift_ContactSelection_SelectAll.uppercased() } let header: ListViewItemHeader? = ChatListSearchItemHeader(type: .text(title.uppercased(), AnyHashable(10 * sectionId + (allSelected ? 1 : 0))), theme: theme, strings: strings, actionTitle: actionTitle, action: { _ in diff --git a/submodules/Display/Source/TextAlertController.swift b/submodules/Display/Source/TextAlertController.swift index c8ddef511c..291446f04b 100644 --- a/submodules/Display/Source/TextAlertController.swift +++ b/submodules/Display/Source/TextAlertController.swift @@ -129,7 +129,7 @@ public final class TextAlertContentActionNode: HighlightableButtonNode { if let range = attributedString.string.range(of: "$") { attributedString.addAttribute(.attachment, value: UIImage(bundleImageName: "Item List/PremiumIcon")!, range: NSRange(range, in: attributedString.string)) attributedString.addAttribute(.foregroundColor, value: color, range: NSRange(range, in: attributedString.string)) - attributedString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: attributedString.string)) + attributedString.addAttribute(.baselineOffset, value: 2.0, range: NSRange(range, in: attributedString.string)) } self.setAttributedTitle(attributedString, for: []) diff --git a/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift b/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift index 0c955d674c..fe3759663f 100644 --- a/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift +++ b/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift @@ -28,6 +28,8 @@ private let productIdentifiers = [ "org.telegram.telegramPremium.sixMonths.code_x10", "org.telegram.telegramPremium.twelveMonths.code_x10", + "org.telegram.telegramPremium.oneWeek.auth", + "org.telegram.telegramStars.topup.x15", "org.telegram.telegramStars.topup.x25", "org.telegram.telegramStars.topup.x50", @@ -216,7 +218,7 @@ public final class InAppPurchaseManager: NSObject { case deferred } - private let engine: TelegramEngine + private let engine: SomeTelegramEngine private var products: [Product] = [] private var productsPromise = Promise<[Product]>([]) @@ -231,7 +233,9 @@ public final class InAppPurchaseManager: NSObject { private let disposableSet = DisposableDict() - public init(engine: TelegramEngine) { + private var lastRequestTimestamp: Double? + + public init(engine: SomeTelegramEngine) { self.engine = engine super.init() @@ -255,11 +259,15 @@ public final class InAppPurchaseManager: NSObject { productRequest.start() self.productRequest = productRequest + self.lastRequestTimestamp = CFAbsoluteTimeGetCurrent() } public var availableProducts: Signal<[Product], NoError> { - if self.products.isEmpty && self.productRequest == nil { - self.requestProducts() + if self.products.isEmpty { + if let lastRequestTimestamp, CFAbsoluteTimeGetCurrent() - lastRequestTimestamp > 10.0 { + Logger.shared.log("InAppPurchaseManager", "No available products, rerequest") + self.requestProducts() + } } return self.productsPromise.get() } @@ -287,7 +295,13 @@ public final class InAppPurchaseManager: NSObject { return .fail(.cantMakePayments) } - let accountPeerId = "\(self.engine.account.peerId.toInt64())" + let accountPeerId: String + switch self.engine { + case let .authorized(engine): + accountPeerId = "\(engine.account.peerId.toInt64())" + case let .unauthorized(engine): + accountPeerId = "\(engine.account.id.int64)" + } Logger.shared.log("InAppPurchaseManager", "Buying: account \(accountPeerId), product \(product.skProduct.productIdentifier), price \(product.price)") @@ -399,7 +413,13 @@ private func getReceiptData() -> Data? { extension InAppPurchaseManager: SKPaymentTransactionObserver { public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { self.stateQueue.async { - let accountPeerId = "\(self.engine.account.peerId.toInt64())" + let accountPeerId: String + switch self.engine { + case let .authorized(engine): + accountPeerId = "\(engine.account.peerId.toInt64())" + case let .unauthorized(engine): + accountPeerId = "\(engine.account.id.int64)" + } let paymentContexts = self.paymentContexts @@ -519,7 +539,12 @@ extension InAppPurchaseManager: SKPaymentTransactionObserver { (purpose |> castError(AssignAppStoreTransactionError.self) |> mapToSignal { purpose -> Signal in - return self.engine.payments.sendAppStoreReceipt(receipt: receiptData, purpose: purpose) + switch self.engine { + case let .authorized(engine): + return engine.payments.sendAppStoreReceipt(receipt: receiptData, purpose: purpose) + case let .unauthorized(engine): + return engine.payments.sendAppStoreReceipt(receipt: receiptData, purpose: purpose) + } }).start(error: { [weak self] _ in Logger.shared.log("InAppPurchaseManager", "Account \(accountPeerId), transactions [\(transactionIds)] failed to assign") for transaction in transactions { @@ -551,8 +576,15 @@ extension InAppPurchaseManager: SKPaymentTransactionObserver { self.onRestoreCompletion = nil if let receiptData = getReceiptData() { + let signal: Signal + switch self.engine { + case let .authorized(engine): + signal = engine.payments.sendAppStoreReceipt(receipt: receiptData, purpose: .restore) + case let .unauthorized(engine): + signal = engine.payments.sendAppStoreReceipt(receipt: receiptData, purpose: .restore) + } self.disposableSet.set( - self.engine.payments.sendAppStoreReceipt(receipt: receiptData, purpose: .restore).start(error: { error in + signal.start(error: { error in Queue.mainQueue().async { if case .serverProvided = error { onRestoreCompletion(.succeed(true)) @@ -586,14 +618,17 @@ extension InAppPurchaseManager: SKPaymentTransactionObserver { } private func debugSaveReceipt(receiptData: Data) { + guard case let .authorized(engine) = self.engine else { + return + } let id = Int64.random(in: Int64.min ... Int64.max) let fileResource = LocalFileMediaResource(fileId: id, size: Int64(receiptData.count), isSecretRelated: false) - self.engine.account.postbox.mediaBox.storeResourceData(fileResource.id, data: receiptData) + engine.account.postbox.mediaBox.storeResourceData(fileResource.id, data: receiptData) let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(receiptData.count), attributes: [.FileName(fileName: "Receipt.dat")], alternativeRepresentations: []) let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) - let _ = enqueueMessages(account: self.engine.account, peerId: self.engine.account.peerId, messages: [message]).start() + let _ = enqueueMessages(account: engine.account, peerId: engine.account.peerId, messages: [message]).start() } } @@ -625,6 +660,9 @@ private final class PendingInAppPurchaseState: Codable { case users case text case entities + case restore + case phoneNumber + case phoneCodeHash } enum PurposeType: Int32 { @@ -637,6 +675,7 @@ private final class PendingInAppPurchaseState: Codable { case stars case starsGift case starsGiveaway + case authCode } case subscription @@ -648,6 +687,7 @@ private final class PendingInAppPurchaseState: Codable { case stars(count: Int64) case starsGift(peerId: EnginePeer.Id, count: Int64) case starsGiveaway(stars: Int64, boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, showWinners: Bool, prizeDescription: String?, randomId: Int64, untilDate: Int32, users: Int32) + case authCode(restore: Bool, phoneNumber: String, phoneCodeHash: String) public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) @@ -704,6 +744,12 @@ private final class PendingInAppPurchaseState: Codable { untilDate: try container.decode(Int32.self, forKey: .untilDate), users: try container.decode(Int32.self, forKey: .users) ) + case .authCode: + self = .authCode( + restore: try container.decode(Bool.self, forKey: .restore), + phoneNumber: try container.decode(String.self, forKey: .phoneNumber), + phoneCodeHash: try container.decode(String.self, forKey: .phoneCodeHash) + ) default: throw DecodingError.generic } @@ -757,6 +803,11 @@ private final class PendingInAppPurchaseState: Codable { try container.encode(randomId, forKey: .randomId) try container.encode(untilDate, forKey: .untilDate) try container.encode(users, forKey: .users) + case let .authCode(restore, phoneNumber, phoneCodeHash): + try container.encode(PurposeType.authCode.rawValue, forKey: .type) + try container.encode(restore, forKey: .restore) + try container.encode(phoneNumber, forKey: .phoneNumber) + try container.encode(phoneCodeHash, forKey: .phoneCodeHash) } } @@ -780,6 +831,8 @@ private final class PendingInAppPurchaseState: Codable { self = .starsGift(peerId: peerId, count: count) case let .starsGiveaway(stars, boostPeer, additionalPeerIds, countries, onlyNewSubscribers, showWinners, prizeDescription, randomId, untilDate, _, _, users): self = .starsGiveaway(stars: stars, boostPeer: boostPeer, additionalPeerIds: additionalPeerIds, countries: countries, onlyNewSubscribers: onlyNewSubscribers, showWinners: showWinners, prizeDescription: prizeDescription, randomId: randomId, untilDate: untilDate, users: users) + case let .authCode(restore, phoneNumber, phoneCodeHash, _, _): + self = .authCode(restore: restore, phoneNumber: phoneNumber, phoneCodeHash: phoneCodeHash) } } @@ -804,6 +857,8 @@ private final class PendingInAppPurchaseState: Codable { return .starsGift(peerId: peerId, count: count, currency: currency, amount: amount) case let .starsGiveaway(stars, boostPeer, additionalPeerIds, countries, onlyNewSubscribers, showWinners, prizeDescription, randomId, untilDate, users): return .starsGiveaway(stars: stars, boostPeer: boostPeer, additionalPeerIds: additionalPeerIds, countries: countries, onlyNewSubscribers: onlyNewSubscribers, showWinners: showWinners, prizeDescription: prizeDescription, randomId: randomId, untilDate: untilDate, currency: currency, amount: amount, users: users) + case let .authCode(restore, phoneNumber, phoneCodeHash): + return .authCode(restore: restore, phoneNumber: phoneNumber, phoneCodeHash: phoneCodeHash, currency: currency, amount: amount) } } } @@ -831,23 +886,41 @@ private final class PendingInAppPurchaseState: Codable { } } -private func pendingInAppPurchaseState(engine: TelegramEngine, productId: String) -> Signal { +private func pendingInAppPurchaseState(engine: SomeTelegramEngine, productId: String) -> Signal { let key = EngineDataBuffer(length: 8) key.setInt64(0, value: Int64(bitPattern: productId.persistentHashValue)) - return engine.data.get(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: ApplicationSpecificItemCacheCollectionId.pendingInAppPurchaseState, id: key)) - |> map { entry -> PendingInAppPurchaseState? in - return entry?.get(PendingInAppPurchaseState.self) + switch engine { + case let .authorized(engine): + return engine.data.get(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: ApplicationSpecificItemCacheCollectionId.pendingInAppPurchaseState, id: key)) + |> map { entry -> PendingInAppPurchaseState? in + return entry?.get(PendingInAppPurchaseState.self) + } + case let .unauthorized(engine): + return engine.itemCache.get(collectionId: ApplicationSpecificItemCacheCollectionId.pendingInAppPurchaseState, id: key) + |> map { entry -> PendingInAppPurchaseState? in + return entry?.get(PendingInAppPurchaseState.self) + } } } -private func updatePendingInAppPurchaseState(engine: TelegramEngine, productId: String, content: PendingInAppPurchaseState?) -> Signal { +private func updatePendingInAppPurchaseState(engine: SomeTelegramEngine, productId: String, content: PendingInAppPurchaseState?) -> Signal { let key = EngineDataBuffer(length: 8) key.setInt64(0, value: Int64(bitPattern: productId.persistentHashValue)) - if let content = content { - return engine.itemCache.put(collectionId: ApplicationSpecificItemCacheCollectionId.pendingInAppPurchaseState, id: key, item: content) - } else { - return engine.itemCache.remove(collectionId: ApplicationSpecificItemCacheCollectionId.pendingInAppPurchaseState, id: key) + + switch engine { + case let .authorized(engine): + if let content = content { + return engine.itemCache.put(collectionId: ApplicationSpecificItemCacheCollectionId.pendingInAppPurchaseState, id: key, item: content) + } else { + return engine.itemCache.remove(collectionId: ApplicationSpecificItemCacheCollectionId.pendingInAppPurchaseState, id: key) + } + case let .unauthorized(engine): + if let content = content { + return engine.itemCache.put(collectionId: ApplicationSpecificItemCacheCollectionId.pendingInAppPurchaseState, id: key, item: content) + } else { + return engine.itemCache.remove(collectionId: ApplicationSpecificItemCacheCollectionId.pendingInAppPurchaseState, id: key) + } } } diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaEditingContext.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaEditingContext.h index efdee05775..ba44c28837 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaEditingContext.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaEditingContext.h @@ -67,9 +67,7 @@ - (SSignal *)coverImageSignalForItem:(NSObject *)item; - (void)setCoverImage:(UIImage *)image position:(NSNumber *)position forItem:(id)item; - (UIImage *)coverImageForItem:(NSObject *)item; - - (NSNumber *)coverPositionForItem:(NSObject *)item; -- (void)setCoverImage:(UIImage *)image position:(NSNumber *)position forItem:(id)item; - (void)setTemporaryRep:(id)rep forItem:(id)item; diff --git a/submodules/MtProtoKit/Sources/MTRequestMessageService.m b/submodules/MtProtoKit/Sources/MTRequestMessageService.m index 27100eeecd..99369c796c 100644 --- a/submodules/MtProtoKit/Sources/MTRequestMessageService.m +++ b/submodules/MtProtoKit/Sources/MTRequestMessageService.m @@ -898,7 +898,7 @@ } restartRequest = true; } - else if (rpcError.errorCode == 420 || [rpcError.errorDescription rangeOfString:@"FLOOD_WAIT_"].location != NSNotFound || [rpcError.errorDescription rangeOfString:@"FLOOD_PREMIUM_WAIT_"].location != NSNotFound) { + else if ((rpcError.errorCode == 420 && [rpcError.errorDescription rangeOfString:@"FROZEN_METHOD_INVALID"].location == NSNotFound) || [rpcError.errorDescription rangeOfString:@"FLOOD_WAIT_"].location != NSNotFound || [rpcError.errorDescription rangeOfString:@"FLOOD_PREMIUM_WAIT_"].location != NSNotFound) { if (request.errorContext == nil) request.errorContext = [[MTRequestErrorContext alloc] init]; diff --git a/submodules/PremiumUI/BUILD b/submodules/PremiumUI/BUILD index 9cad24de43..6254850f9e 100644 --- a/submodules/PremiumUI/BUILD +++ b/submodules/PremiumUI/BUILD @@ -120,6 +120,7 @@ swift_library( "//submodules/TelegramUI/Components/EmojiActionIconComponent", "//submodules/TelegramUI/Components/ScrollComponent", "//submodules/TelegramUI/Components/Premium/PremiumStarComponent", + "//submodules/TelegramUI/Components/Premium/PremiumCoinComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/PremiumUI/Sources/BusinessPageComponent.swift b/submodules/PremiumUI/Sources/BusinessPageComponent.swift index 72ae9e5639..46c469d7ae 100644 --- a/submodules/PremiumUI/Sources/BusinessPageComponent.swift +++ b/submodules/PremiumUI/Sources/BusinessPageComponent.swift @@ -11,6 +11,7 @@ import Markdown import TelegramPresentationData import BundleIconComponent import ScrollComponent +import PremiumCoinComponent private final class HeaderComponent: Component { let context: AccountContext diff --git a/submodules/PremiumUI/Sources/EmojiHeaderComponent.swift b/submodules/PremiumUI/Sources/EmojiHeaderComponent.swift index cba245039f..59dd08228d 100644 --- a/submodules/PremiumUI/Sources/EmojiHeaderComponent.swift +++ b/submodules/PremiumUI/Sources/EmojiHeaderComponent.swift @@ -64,6 +64,7 @@ class EmojiHeaderComponent: Component { } weak var animateFrom: UIView? + var sourceRect: CGRect? weak var containerView: UIView? let statusView: ComponentHostView @@ -116,8 +117,13 @@ class EmojiHeaderComponent: Component { let initialPosition = self.statusView.center let targetPosition = self.statusView.superview!.convert(self.statusView.center, to: containerView) - let sourcePosition = animateFrom.superview!.convert(animateFrom.center, to: containerView).offsetBy(dx: 0.0, dy: 0.0) + var sourceOffset: CGPoint = .zero + if let sourceRect = self.sourceRect { + sourceOffset = CGPoint(x: sourceRect.center.x - animateFrom.frame.width / 2.0, y: 0.0) + } + let sourcePosition = animateFrom.superview!.convert(animateFrom.center, to: containerView).offsetBy(dx: sourceOffset.x, dy: sourceOffset.y) + containerView.addSubview(self.statusView) self.statusView.center = targetPosition @@ -127,6 +133,7 @@ class EmojiHeaderComponent: Component { self.statusView.layer.animatePosition(from: sourcePosition, to: targetPosition, duration: 0.55, timingFunction: kCAMediaTimingFunctionSpring) Queue.mainQueue().after(0.55, { + self.statusView.layer.removeAllAnimations() self.addSubview(self.statusView) self.statusView.center = initialPosition }) diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index 7d0d43370a..c96c0668d6 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -34,6 +34,7 @@ import EntityKeyboard import EmojiActionIconComponent import ScrollComponent import PremiumStarComponent +import PremiumCoinComponent public enum PremiumSource: Equatable { public static func == (lhs: PremiumSource, rhs: PremiumSource) -> Bool { @@ -308,6 +309,12 @@ public enum PremiumSource: Equatable { } else { return false } + case let .auth(lhsPrice): + if case let .auth(rhsPrice) = rhs, lhsPrice == rhsPrice { + return true + } else { + return false + } } } @@ -356,6 +363,7 @@ public enum PremiumSource: Equatable { case folderTags case messageEffects case paidMessages + case auth(String) var identifier: String? { switch self { @@ -451,6 +459,8 @@ public enum PremiumSource: Equatable { return "effects" case .paidMessages: return "paid_messages" + case .auth: + return "auth" } } } @@ -1213,6 +1223,7 @@ final class PerkComponent: CombinedComponent { let subtitleColor: UIColor let arrowColor: UIColor let accentColor: UIColor + let displayArrow: Bool let badge: String? init( @@ -1224,6 +1235,7 @@ final class PerkComponent: CombinedComponent { subtitleColor: UIColor, arrowColor: UIColor, accentColor: UIColor, + displayArrow: Bool = true, badge: String? = nil ) { self.iconName = iconName @@ -1234,6 +1246,7 @@ final class PerkComponent: CombinedComponent { self.subtitleColor = subtitleColor self.arrowColor = arrowColor self.accentColor = accentColor + self.displayArrow = displayArrow self.badge = badge } @@ -1262,6 +1275,9 @@ final class PerkComponent: CombinedComponent { if lhs.accentColor != rhs.accentColor { return false } + if lhs.displayArrow != rhs.displayArrow { + return false + } if lhs.badge != rhs.badge { return false } @@ -1304,16 +1320,7 @@ final class PerkComponent: CombinedComponent { availableSize: iconSize, transition: context.transition ) - - let arrow = arrow.update( - component: BundleIconComponent( - name: "Item List/DisclosureArrow", - tintColor: component.arrowColor - ), - availableSize: context.availableSize, - transition: context.transition - ) - + let title = title.update( component: MultilineTextComponent( text: .plain( @@ -1390,9 +1397,20 @@ final class PerkComponent: CombinedComponent { ) let size = CGSize(width: context.availableSize.width, height: textTopInset + title.size.height + spacing + subtitle.size.height + textBottomInset) - context.add(arrow - .position(CGPoint(x: context.availableSize.width - 7.0 - arrow.size.width / 2.0, y: size.height / 2.0)) - ) + + if component.displayArrow { + let arrow = arrow.update( + component: BundleIconComponent( + name: "Item List/DisclosureArrow", + tintColor: component.arrowColor + ), + availableSize: context.availableSize, + transition: context.transition + ) + context.add(arrow + .position(CGPoint(x: context.availableSize.width - 7.0 - arrow.size.width / 2.0, y: size.height / 2.0)) + ) + } return size } @@ -1403,7 +1421,7 @@ final class PerkComponent: CombinedComponent { private final class PremiumIntroScreenContentComponent: CombinedComponent { typealias EnvironmentType = (ViewControllerComponentContainer.Environment, ScrollChildEnvironment) - let context: AccountContext + let screenContext: PremiumIntroScreen.ScreenContext let mode: PremiumIntroScreen.Mode let source: PremiumSource let forceDark: Bool @@ -1423,7 +1441,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { let shareLink: (String) -> Void init( - context: AccountContext, + screenContext: PremiumIntroScreen.ScreenContext, mode: PremiumIntroScreen.Mode, source: PremiumSource, forceDark: Bool, @@ -1442,7 +1460,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { copyLink: @escaping (String) -> Void, shareLink: @escaping (String) -> Void ) { - self.context = context + self.screenContext = screenContext self.mode = mode self.source = source self.forceDark = forceDark @@ -1463,9 +1481,6 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { } static func ==(lhs: PremiumIntroScreenContentComponent, rhs: PremiumIntroScreenContentComponent) -> Bool { - if lhs.context !== rhs.context { - return false - } if lhs.source != rhs.source { return false } @@ -1498,7 +1513,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { } final class State: ComponentState { - private let context: AccountContext + private let screenContext: PremiumIntroScreen.ScreenContext private let present: (ViewController) -> Void var products: [PremiumProduct]? @@ -1542,104 +1557,122 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { var cachedChevronImage: (UIImage, PresentationTheme)? init( - context: AccountContext, + screenContext: PremiumIntroScreen.ScreenContext, source: PremiumSource, present: @escaping (ViewController) -> Void ) { - self.context = context + self.screenContext = screenContext self.present = present super.init() - self.disposable = (context.engine.data.subscribe( - TelegramEngine.EngineData.Item.Configuration.App(), - TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId) - ) - |> deliverOnMainQueue).start(next: { [weak self] appConfiguration, accountPeer in - if let strongSelf = self { - let isFirstTime = strongSelf.peer == nil + let premiumIntroConfiguration: Signal + let accountPeer: Signal + switch screenContext { + case let .accountContext(context): + premiumIntroConfiguration = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.App()) + |> map { appConfiguration in + return PremiumIntroConfiguration.with(appConfiguration: appConfiguration) + } + accountPeer = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + case .sharedContext: + premiumIntroConfiguration = .single(PremiumIntroConfiguration.defaultValue) + accountPeer = .single(nil) + } + + self.disposable = combineLatest( + queue: Queue.mainQueue(), + premiumIntroConfiguration, + accountPeer + ).start(next: { [weak self] premiumIntroConfiguration, accountPeer in + guard let self else { + return + } + let isFirstTime = self.peer == nil + + self.configuration = premiumIntroConfiguration + self.peer = accountPeer + self.updated(transition: .immediate) + + if let identifier = source.identifier, isFirstTime { + var jsonString: String = "{" + jsonString += "\"source\": \"\(identifier)\"," - strongSelf.configuration = PremiumIntroConfiguration.with(appConfiguration: appConfiguration) - strongSelf.peer = accountPeer - strongSelf.updated(transition: .immediate) + jsonString += "\"data\": {\"premium_promo_order\":[" + var isFirst = true + for perk in premiumIntroConfiguration.perks { + if !isFirst { + jsonString += "," + } + isFirst = false + jsonString += "\"\(perk.identifier)\"" + } + jsonString += "]}}" - if let identifier = source.identifier, isFirstTime { - var jsonString: String = "{" - jsonString += "\"source\": \"\(identifier)\"," - - jsonString += "\"data\": {\"premium_promo_order\":[" - var isFirst = true - for perk in strongSelf.configuration.perks { - if !isFirst { - jsonString += "," - } - isFirst = false - jsonString += "\"\(perk.identifier)\"" - } - jsonString += "]}}" - - if let data = jsonString.data(using: .utf8), let json = JSON(data: data) { - addAppLogEvent(postbox: strongSelf.context.account.postbox, type: "premium.promo_screen_show", data: json) - } + if let context = screenContext.context, let data = jsonString.data(using: .utf8), let json = JSON(data: data) { + addAppLogEvent(postbox: context.account.postbox, type: "premium.promo_screen_show", data: json) } } }) - let _ = updatePremiumPromoConfigurationOnce(account: context.account).start() - - let stickersKey: PostboxViewKey = .orderedItemList(id: Namespaces.OrderedItemList.CloudPremiumStickers) - self.stickersDisposable = (self.context.account.postbox.combinedView(keys: [stickersKey]) - |> deliverOnMainQueue).start(next: { [weak self] views in - guard let strongSelf = self else { - return - } - if let view = views.views[stickersKey] as? OrderedItemListView { - for item in view.items { - if let mediaItem = item.contents.get(RecentMediaItem.self) { - let file = mediaItem.media._parse() - strongSelf.preloadDisposableSet.add(freeMediaFileResourceInteractiveFetched(account: context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start()) - if let effect = file.videoThumbnails.first { - strongSelf.preloadDisposableSet.add(freeMediaFileResourceInteractiveFetched(account: context.account, userLocation: .other, fileReference: .standalone(media: file), resource: effect.resource).start()) + if let context = screenContext.context { + let _ = updatePremiumPromoConfigurationOnce(account: context.account).start() + + let stickersKey: PostboxViewKey = .orderedItemList(id: Namespaces.OrderedItemList.CloudPremiumStickers) + self.stickersDisposable = (context.account.postbox.combinedView(keys: [stickersKey]) + |> deliverOnMainQueue).start(next: { [weak self] views in + guard let self else { + return + } + if let view = views.views[stickersKey] as? OrderedItemListView { + for item in view.items { + if let mediaItem = item.contents.get(RecentMediaItem.self) { + let file = mediaItem.media._parse() + self.preloadDisposableSet.add(freeMediaFileResourceInteractiveFetched(account: context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start()) + if let effect = file.videoThumbnails.first { + self.preloadDisposableSet.add(freeMediaFileResourceInteractiveFetched(account: context.account, userLocation: .other, fileReference: .standalone(media: file), resource: effect.resource).start()) + } } } } - } - }) - - self.newPerksDisposable = combineLatest(queue: Queue.mainQueue(), - ApplicationSpecificNotice.dismissedBusinessBadge(accountManager: context.sharedContext.accountManager), - ApplicationSpecificNotice.dismissedBusinessLinksBadge(accountManager: context.sharedContext.accountManager), - ApplicationSpecificNotice.dismissedBusinessIntroBadge(accountManager: context.sharedContext.accountManager), - ApplicationSpecificNotice.dismissedBusinessChatbotsBadge(accountManager: context.sharedContext.accountManager) - ).startStrict(next: { [weak self] dismissedBusinessBadge, dismissedBusinessLinksBadge, dismissedBusinessIntroBadge, dismissedBusinessChatbotsBadge in - guard let self else { - return - } - var newPerks: [String] = [] - if !dismissedBusinessBadge { - newPerks.append(PremiumPerk.business.identifier) - } - if !dismissedBusinessLinksBadge { - newPerks.append(PremiumPerk.businessLinks.identifier) - } - if !dismissedBusinessIntroBadge { - newPerks.append(PremiumPerk.businessIntro.identifier) - } - if !dismissedBusinessChatbotsBadge { - newPerks.append(PremiumPerk.businessChatBots.identifier) - } - self.newPerks = newPerks - self.updated() - }) - - self.adsEnabledDisposable = (context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.AdsEnabled(id: context.account.peerId)) - |> deliverOnMainQueue).start(next: { [weak self] adsEnabled in - guard let self else { - return - } - self.adsEnabled = adsEnabled - self.updated() - }) + }) + + self.newPerksDisposable = combineLatest( + queue: Queue.mainQueue(), + ApplicationSpecificNotice.dismissedBusinessBadge(accountManager: context.sharedContext.accountManager), + ApplicationSpecificNotice.dismissedBusinessLinksBadge(accountManager: context.sharedContext.accountManager), + ApplicationSpecificNotice.dismissedBusinessIntroBadge(accountManager: context.sharedContext.accountManager), + ApplicationSpecificNotice.dismissedBusinessChatbotsBadge(accountManager: context.sharedContext.accountManager) + ).startStrict(next: { [weak self] dismissedBusinessBadge, dismissedBusinessLinksBadge, dismissedBusinessIntroBadge, dismissedBusinessChatbotsBadge in + guard let self else { + return + } + var newPerks: [String] = [] + if !dismissedBusinessBadge { + newPerks.append(PremiumPerk.business.identifier) + } + if !dismissedBusinessLinksBadge { + newPerks.append(PremiumPerk.businessLinks.identifier) + } + if !dismissedBusinessIntroBadge { + newPerks.append(PremiumPerk.businessIntro.identifier) + } + if !dismissedBusinessChatbotsBadge { + newPerks.append(PremiumPerk.businessChatBots.identifier) + } + self.newPerks = newPerks + self.updated() + }) + + self.adsEnabledDisposable = (context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.AdsEnabled(id: context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] adsEnabled in + guard let self else { + return + } + self.adsEnabled = adsEnabled + self.updated() + }) + } } deinit { @@ -1655,6 +1688,9 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { private weak var emojiStatusSelectionController: ViewController? private var previousEmojiSetupTimestamp: Double? func openEmojiSetup(sourceView: UIView, currentFileId: Int64?, color: UIColor?) { + guard let context = self.screenContext.context else { + return + } let currentTimestamp = CACurrentMediaTime() if let previousTimestamp = self.previousEmojiSetupTimestamp, currentTimestamp < previousTimestamp + 1.0 { return @@ -1668,20 +1704,20 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { } let controller = EmojiStatusSelectionController( - context: self.context, + context: context, mode: .statusSelection, sourceView: sourceView, emojiContent: EmojiPagerContentComponent.emojiInputData( - context: self.context, - animationCache: self.context.animationCache, - animationRenderer: self.context.animationRenderer, + context: context, + animationCache: context.animationCache, + animationRenderer: context.animationRenderer, isStandalone: false, subject: .status, hasTrending: false, topReactionItems: [], areUnicodeEmojiEnabled: false, areCustomEmojiEnabled: true, - chatPeerId: self.context.account.peerId, + chatPeerId: context.account.peerId, selectedItems: selectedItems, topStatusTitle: nil, backgroundIconColor: color @@ -1701,7 +1737,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { } func makeState() -> State { - return State(context: self.context, source: self.source, present: self.present) + return State(screenContext: self.screenContext, source: self.source, present: self.present) } static var body: Body { @@ -1733,7 +1769,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { let theme = environment.theme let strings = environment.strings - let presentationData = context.component.context.sharedContext.currentPresentationData.with { $0 } + let presentationData = context.component.screenContext.presentationData let availableWidth = context.availableSize.width let sideInsets = sideInset * 2.0 + environment.safeInsets.left + environment.safeInsets.right @@ -1786,8 +1822,8 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { } else if case .giftTerms = context.component.source { textString = strings.Premium_PersonalDescription } else if let _ = context.component.otherPeerName { - if case let .gift(fromId, _, _, giftCode) = context.component.source { - if fromId == context.component.context.account.peerId { + if case let .gift(fromId, _, _, giftCode) = context.component.source, let accountContext = context.component.screenContext.context { + if fromId == accountContext.account.peerId { textString = strings.Premium_GiftedDescriptionYou } else { if let giftCode { @@ -1895,7 +1931,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { UIColor(rgb: 0x3dbd4a) ] - let accountContext = context.component.context + let accountContext = context.component.screenContext.context let present = context.component.present let push = context.component.push let selectProduct = context.component.selectProduct @@ -2067,7 +2103,11 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { foregroundColor: .white, iconName: perk.iconName ))), false), + accessory: accountContext != nil ? .arrow : nil, action: { [weak state] _ in + guard let accountContext else { + return + } var demoSubject: PremiumDemoScreen.Subject switch perk { case .doubleLimits: @@ -2140,7 +2180,8 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { updateIsFocused(true) addAppLogEvent(postbox: accountContext.account.postbox, type: "premium.promo_screen_tap", data: ["item": perk.identifier]) - } + }, + highlighting: accountContext != nil ? .default : .disabled )))) i += 1 } @@ -2239,6 +2280,10 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { iconName: perk.iconName ))), false), action: { [weak state] _ in + guard let accountContext else { + return + } + let isPremium = state?.isPremium == true if isPremium { switch perk { @@ -2396,46 +2441,48 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { let accentColor = environment.theme.list.itemAccentColor var perksItems: [AnyComponentWithIdentity] = [] - perksItems.append(AnyComponentWithIdentity(id: perksItems.count, component: AnyComponent(ListActionItemComponent( - theme: environment.theme, - title: AnyComponent(VStack([ - AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: strings.Business_SetEmojiStatus, - font: Font.regular(presentationData.listsFontSize.baseDisplaySize), - textColor: environment.theme.list.itemPrimaryTextColor - )), - maximumNumberOfLines: 0 - ))), - AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: strings.Business_SetEmojiStatusInfo, - font: Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0)), - textColor: environment.theme.list.itemSecondaryTextColor - )), - maximumNumberOfLines: 0, - lineSpacing: 0.18 - ))) - ], alignment: .left, spacing: 2.0)), - leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(PerkIconComponent( - backgroundColor: UIColor(rgb: 0x676bff), - foregroundColor: .white, - iconName: "Premium/BusinessPerk/Status" - ))), false), - icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(EmojiActionIconComponent( - context: context.component.context, - color: accentColor, - fileId: status?.fileId, - file: nil - )))), - accessory: nil, - action: { [weak state] view in - guard let view = view as? ListActionItemComponent.View, let iconView = view.iconView else { - return + if let accountContext = context.component.screenContext.context { + perksItems.append(AnyComponentWithIdentity(id: perksItems.count, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: strings.Business_SetEmojiStatus, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 0 + ))), + AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: strings.Business_SetEmojiStatusInfo, + font: Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0)), + textColor: environment.theme.list.itemSecondaryTextColor + )), + maximumNumberOfLines: 0, + lineSpacing: 0.18 + ))) + ], alignment: .left, spacing: 2.0)), + leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(PerkIconComponent( + backgroundColor: UIColor(rgb: 0x676bff), + foregroundColor: .white, + iconName: "Premium/BusinessPerk/Status" + ))), false), + icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(EmojiActionIconComponent( + context: accountContext, + color: accentColor, + fileId: status?.fileId, + file: nil + )))), + accessory: nil, + action: { [weak state] view in + guard let view = view as? ListActionItemComponent.View, let iconView = view.iconView else { + return + } + state?.openEmojiSetup(sourceView: iconView, currentFileId: nil, color: accentColor) } - state?.openEmojiSetup(sourceView: iconView, currentFileId: nil, color: accentColor) - } - )))) + )))) + } perksItems.append(AnyComponentWithIdentity(id: perksItems.count, component: AnyComponent(ListActionItemComponent( theme: environment.theme, @@ -2464,6 +2511,9 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { iconName: "Premium/BusinessPerk/Tag" ))), false), action: { _ in + guard let accountContext else { + return + } push(accountContext.sharedContext.makeFilterSettingsController(context: accountContext, modal: false, scrollToTags: true, dismissed: nil)) } )))) @@ -2495,6 +2545,9 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { iconName: "Premium/Perk/Stories" ))), false), action: { _ in + guard let accountContext else { + return + } push(accountContext.sharedContext.makeMyStoriesController(context: accountContext, isArchive: false)) } )))) @@ -2560,6 +2613,9 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { ))), ], alignment: .left, spacing: 2.0)), accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: state.adsEnabled, action: { [weak state] value in + guard let accountContext else { + return + } let _ = accountContext.engine.accountData.updateAdMessagesEnabled(enabled: value).startStandalone() state?.updated(transition: .immediate) })), @@ -2576,8 +2632,8 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { } let controller = environment.controller let adsInfoTapActionImpl: ([NSAttributedString.Key: Any]) -> Void = { _ in - if let controller = controller() as? PremiumIntroScreen { - controller.context.sharedContext.openExternalUrl(context: controller.context, urlContext: .generic, url: environment.strings.Business_AdsInfo_URL, forceExternal: true, presentationData: controller.context.sharedContext.currentPresentationData.with({$0}), navigationController: nil, dismissInput: {}) + if let controller = controller() as? PremiumIntroScreen, let context = controller.context { + context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: environment.strings.Business_AdsInfo_URL, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {}) } } let adsSettingsSection = adsSettingsSection.update( @@ -2627,7 +2683,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { layoutPerks() layoutOptions() } else if case let .gift(fromPeerId, _, _, giftCode) = context.component.source { - if let giftCode, fromPeerId != context.component.context.account.peerId, !context.component.justBought { + if let giftCode, let accountContext = context.component.screenContext.context, fromPeerId != accountContext.account.peerId, !context.component.justBought { let link = "https://t.me/giftcode/\(giftCode.slug)" let linkButton = linkButton.update( component: Button( @@ -2718,7 +2774,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { var isGiftView = false if case let .gift(fromId, _, _, _) = context.component.source { - if fromId == context.component.context.account.peerId { + if let accountContext = context.component.screenContext.context, fromId == accountContext.account.peerId { isGiftView = true } } @@ -2738,14 +2794,12 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { let controller = environment.controller let termsTapActionImpl: ([NSAttributedString.Key: Any]) -> Void = { attributes in - if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String, - let controller = controller() as? PremiumIntroScreen, let navigationController = controller.navigationController as? NavigationController { + if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String, let controller = controller() as? PremiumIntroScreen, let context = controller.context, let navigationController = controller.navigationController as? NavigationController { if url.hasPrefix("https://apps.apple.com/account/subscriptions") { - controller.context.sharedContext.applicationBindings.openSubscriptions() + context.sharedContext.applicationBindings.openSubscriptions() } else if url.hasPrefix("https://") || url.hasPrefix("tg://") { - controller.context.sharedContext.openExternalUrl(context: controller.context, urlContext: .generic, url: url, forceExternal: false, presentationData: controller.context.sharedContext.currentPresentationData.with({$0}), navigationController: navigationController, dismissInput: {}) + context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: url, forceExternal: false, presentationData: presentationData, navigationController: navigationController, dismissInput: {}) } else { - let context = controller.context let signal: Signal? switch url { case "terms": @@ -2815,7 +2869,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { private final class PremiumIntroScreenComponent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment - let context: AccountContext + let screenContext: PremiumIntroScreen.ScreenContext let mode: PremiumIntroScreen.Mode let source: PremiumSource let forceDark: Bool @@ -2827,8 +2881,8 @@ private final class PremiumIntroScreenComponent: CombinedComponent { let copyLink: (String) -> Void let shareLink: (String) -> Void - init(context: AccountContext, mode: PremiumIntroScreen.Mode, source: PremiumSource, forceDark: Bool, forceHasPremium: Bool, updateInProgress: @escaping (Bool) -> Void, present: @escaping (ViewController) -> Void, push: @escaping (ViewController) -> Void, completion: @escaping () -> Void, copyLink: @escaping (String) -> Void, shareLink: @escaping (String) -> Void) { - self.context = context + init(screenContext: PremiumIntroScreen.ScreenContext, mode: PremiumIntroScreen.Mode, source: PremiumSource, forceDark: Bool, forceHasPremium: Bool, updateInProgress: @escaping (Bool) -> Void, present: @escaping (ViewController) -> Void, push: @escaping (ViewController) -> Void, completion: @escaping () -> Void, copyLink: @escaping (String) -> Void, shareLink: @escaping (String) -> Void) { + self.screenContext = screenContext self.mode = mode self.source = source self.forceDark = forceDark @@ -2842,9 +2896,6 @@ private final class PremiumIntroScreenComponent: CombinedComponent { } static func ==(lhs: PremiumIntroScreenComponent, rhs: PremiumIntroScreenComponent) -> Bool { - if lhs.context !== rhs.context { - return false - } if lhs.mode != rhs.mode { return false } @@ -2861,7 +2912,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { } final class State: ComponentState { - private let context: AccountContext + private let screenContext: PremiumIntroScreen.ScreenContext private let source: PremiumSource private let updateInProgress: (Bool) -> Void private let present: (ViewController) -> Void @@ -2883,10 +2934,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { var isPremium: Bool? var otherPeerName: String? var justBought = false - - let animationCache: AnimationCache - let animationRenderer: MultiAnimationRenderer - + var emojiFile: TelegramMediaFile? var emojiPackTitle: String? private var emojiFileDisposable: Disposable? @@ -2917,43 +2965,44 @@ private final class PremiumIntroScreenComponent: CombinedComponent { } } - init(context: AccountContext, source: PremiumSource, forceHasPremium: Bool, updateInProgress: @escaping (Bool) -> Void, present: @escaping (ViewController) -> Void, completion: @escaping () -> Void) { - self.context = context + init(screenContext: PremiumIntroScreen.ScreenContext, source: PremiumSource, forceHasPremium: Bool, updateInProgress: @escaping (Bool) -> Void, present: @escaping (ViewController) -> Void, completion: @escaping () -> Void) { + self.screenContext = screenContext self.source = source self.updateInProgress = updateInProgress self.present = present self.completion = completion - - self.animationCache = context.animationCache - self.animationRenderer = context.animationRenderer - + super.init() - self.validPurchases = context.inAppPurchaseManager?.getReceiptPurchases() ?? [] + self.validPurchases = screenContext.inAppPurchaseManager?.getReceiptPurchases() ?? [] let availableProducts: Signal<[InAppPurchaseManager.Product], NoError> - if let inAppPurchaseManager = context.inAppPurchaseManager { + if let inAppPurchaseManager = screenContext.inAppPurchaseManager { availableProducts = inAppPurchaseManager.availableProducts } else { availableProducts = .single([]) } let otherPeerName: Signal - if case let .gift(fromPeerId, toPeerId, _, _) = source { - let otherPeerId = fromPeerId != context.account.peerId ? fromPeerId : toPeerId - otherPeerName = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: otherPeerId)) - |> map { peer -> String? in - return peer?.compactDisplayTitle - } - } else if case let .profile(peerId) = source { - otherPeerName = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) - |> map { peer -> String? in - return peer?.compactDisplayTitle - } - } else if case let .emojiStatus(peerId, _, _, _) = source { - otherPeerName = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) - |> map { peer -> String? in - return peer?.compactDisplayTitle + if let context = screenContext.context { + if case let .gift(fromPeerId, toPeerId, _, _) = source { + let otherPeerId = fromPeerId != context.account.peerId ? fromPeerId : toPeerId + otherPeerName = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: otherPeerId)) + |> map { peer -> String? in + return peer?.compactDisplayTitle + } + } else if case let .profile(peerId) = source { + otherPeerName = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> map { peer -> String? in + return peer?.compactDisplayTitle + } + } else if case let .emojiStatus(peerId, _, _, _) = source { + otherPeerName = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> map { peer -> String? in + return peer?.compactDisplayTitle + } + } else { + otherPeerName = .single(nil) } } else { otherPeerName = .single(nil) @@ -2963,21 +3012,31 @@ private final class PremiumIntroScreenComponent: CombinedComponent { self.isPremium = true } + let isPremium: Signal + let promoConfiguration: Signal + switch screenContext { + case let .accountContext(context): + isPremium = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> map { peer -> Bool in + return peer?.isPremium ?? false + } + promoConfiguration = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.PremiumPromo()) + case .sharedContext: + isPremium = .single(false) + promoConfiguration = .single(PremiumPromoConfiguration.defaultValue) + } + self.disposable = combineLatest( queue: Queue.mainQueue(), availableProducts, - context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.PremiumPromo()), - context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) - |> map { peer -> Bool in - return peer?.isPremium ?? false - }, + promoConfiguration, + isPremium, otherPeerName ).start(next: { [weak self] availableProducts, promoConfiguration, isPremium, otherPeerName in if let strongSelf = self { strongSelf.promoConfiguration = promoConfiguration let hadProducts = strongSelf.products != nil - var products: [PremiumProduct] = [] for option in promoConfiguration.premiumProductOptions { if let product = availableProducts.first(where: { $0.id == option.storeProductId }), product.isSubscription { @@ -2992,8 +3051,10 @@ private final class PremiumIntroScreenComponent: CombinedComponent { if !hadProducts { strongSelf.selectedProductId = strongSelf.products?.first?.id - for (_, video) in promoConfiguration.videos { - strongSelf.preloadDisposableSet.add(preloadVideoResource(postbox: context.account.postbox, userLocation: .other, userContentType: .video, resourceReference: .standalone(resource: video.resource), duration: 3.0).start()) + if let context = screenContext.context { + for (_, video) in promoConfiguration.videos { + strongSelf.preloadDisposableSet.add(preloadVideoResource(postbox: context.account.postbox, userLocation: .other, userContentType: .video, resourceReference: .standalone(resource: video.resource), duration: 3.0).start()) + } } } @@ -3007,14 +3068,16 @@ private final class PremiumIntroScreenComponent: CombinedComponent { self.emojiPackTitle = info.title self.updated(transition: .immediate) } else { - self.emojiFileDisposable = (context.engine.stickers.resolveInlineStickers(fileIds: [emojiFileId]) - |> deliverOnMainQueue).start(next: { [weak self] result in - guard let strongSelf = self else { - return - } - strongSelf.emojiFile = result[emojiFileId] - strongSelf.updated(transition: .immediate) - }) + if let context = screenContext.context { + self.emojiFileDisposable = (context.engine.stickers.resolveInlineStickers(fileIds: [emojiFileId]) + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let self else { + return + } + self.emojiFile = result[emojiFileId] + self.updated(transition: .immediate) + }) + } } } } @@ -3032,12 +3095,17 @@ private final class PremiumIntroScreenComponent: CombinedComponent { return } + let presentationData = self.screenContext.presentationData + if case let .gift(_, _, _, giftCode) = self.source, let giftCode, giftCode.usedDate == nil { + guard let context = self.screenContext.context else { + return + } self.inProgress = true self.updateInProgress(true) self.updated(transition: .immediate) - self.paymentDisposable.set((self.context.engine.payments.applyPremiumGiftCode(slug: giftCode.slug) + self.paymentDisposable.set((context.engine.payments.applyPremiumGiftCode(slug: giftCode.slug) |> deliverOnMainQueue).start(error: { [weak self] error in guard let self else { return @@ -3048,8 +3116,6 @@ private final class PremiumIntroScreenComponent: CombinedComponent { self.updated(transition: .immediate) if case let .waitForExpiration(date) = error { - let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - let dateText = stringForMediumDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat) self.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 })) } @@ -3067,16 +3133,15 @@ private final class PremiumIntroScreenComponent: CombinedComponent { return } - guard let inAppPurchaseManager = self.context.inAppPurchaseManager, + guard let inAppPurchaseManager = self.screenContext.inAppPurchaseManager, let premiumProduct = self.products?.first(where: { $0.id == self.selectedProductId }) else { return } - let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } let isUpgrade = self.products?.first(where: { $0.isCurrent }) != nil var hasActiveSubsciption = false - if let data = self.context.currentAppConfiguration.with({ $0 }).data, let _ = data["ios_killswitch_disable_receipt_check"] { + if let context = self.screenContext.context, let data = context.currentAppConfiguration.with({ $0 }).data, let _ = data["ios_killswitch_disable_receipt_check"] { } else if !self.validPurchases.isEmpty && !isUpgrade { let now = Date() @@ -3089,26 +3154,40 @@ private final class PremiumIntroScreenComponent: CombinedComponent { if hasActiveSubsciption { let errorText = presentationData.strings.Premium_Purchase_OnlyOneSubscriptionAllowed - let alertController = textAlertController(context: self.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) + let alertController = textAlertController(sharedContext: self.screenContext.sharedContext, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) self.present(alertController) return } - addAppLogEvent(postbox: self.context.account.postbox, type: "premium.promo_screen_accept") - + if let context = self.screenContext.context { + addAppLogEvent(postbox: context.account.postbox, type: "premium.promo_screen_accept") + } + self.inProgress = true self.updateInProgress(true) self.updated(transition: .immediate) let purpose: AppStoreTransactionPurpose = isUpgrade ? .upgrade : .subscription - let _ = (self.context.engine.payments.canPurchasePremium(purpose: purpose) + + let canPurchasePremium: Signal + switch self.screenContext { + case let .accountContext(context): + canPurchasePremium = context.engine.payments.canPurchasePremium(purpose: purpose) + case let .sharedContext(_, engine, _): + canPurchasePremium = engine.payments.canPurchasePremium(purpose: purpose) + } + let _ = (canPurchasePremium |> deliverOnMainQueue).start(next: { [weak self] available in - if let strongSelf = self { - if available { - strongSelf.paymentDisposable.set((inAppPurchaseManager.buyProduct(premiumProduct.storeProduct, purpose: purpose) - |> deliverOnMainQueue).start(next: { [weak self] status in - if let strongSelf = self, case .purchased = status { - strongSelf.activationDisposable.set((strongSelf.context.account.postbox.peerView(id: strongSelf.context.account.peerId) + guard let self else { + return + } + if available { + self.paymentDisposable.set((inAppPurchaseManager.buyProduct(premiumProduct.storeProduct, purpose: purpose) + |> deliverOnMainQueue).start(next: { [weak self] status in + if let self, case .purchased = status { + let activation: Signal + if let context = self.screenContext.context { + activation = context.account.postbox.peerView(id: context.account.peerId) |> castError(AssignAppStoreTransactionError.self) |> take(until: { view in if let peer = view.peers[view.peerId], peer.isPremium { @@ -3121,70 +3200,82 @@ private final class PremiumIntroScreenComponent: CombinedComponent { return .never() } |> timeout(15.0, queue: Queue.mainQueue(), alternate: .fail(.timeout)) - |> deliverOnMainQueue).start(error: { [weak self] _ in - if let strongSelf = self { - strongSelf.inProgress = false - strongSelf.updateInProgress(false) - - strongSelf.updated(transition: .immediate) - - addAppLogEvent(postbox: strongSelf.context.account.postbox, type: "premium.promo_screen_fail") - - let errorText = presentationData.strings.Premium_Purchase_ErrorUnknown - let alertController = textAlertController(context: strongSelf.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) - strongSelf.present(alertController) - } - }, completed: { [weak self] in - if let strongSelf = self { - let _ = updatePremiumPromoConfigurationOnce(account: strongSelf.context.account).start() - strongSelf.inProgress = false - strongSelf.updateInProgress(false) - - strongSelf.isPremium = true - strongSelf.justBought = true - - strongSelf.updated(transition: .easeInOut(duration: 0.25)) - strongSelf.completion() - } - })) + } else { + activation = .complete() } - }, error: { [weak self] error in - if let strongSelf = self { - strongSelf.inProgress = false - strongSelf.updateInProgress(false) - strongSelf.updated(transition: .immediate) - - var errorText: String? - switch error { - case .generic: - errorText = presentationData.strings.Premium_Purchase_ErrorUnknown - case .network: - errorText = presentationData.strings.Premium_Purchase_ErrorNetwork - case .notAllowed: - errorText = presentationData.strings.Premium_Purchase_ErrorNotAllowed - case .cantMakePayments: - errorText = presentationData.strings.Premium_Purchase_ErrorCantMakePayments - case .assignFailed: - errorText = presentationData.strings.Premium_Purchase_ErrorUnknown - case .tryLater: - errorText = presentationData.strings.Premium_Purchase_ErrorUnknown - case .cancelled: - break - } - - if let errorText = errorText { - addAppLogEvent(postbox: strongSelf.context.account.postbox, type: "premium.promo_screen_fail") + + self.activationDisposable.set((activation + |> deliverOnMainQueue).start(error: { [weak self] _ in + if let self { + self.inProgress = false + self.updateInProgress(false) - let alertController = textAlertController(context: strongSelf.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) - strongSelf.present(alertController) + self.updated(transition: .immediate) + + if let context = self.screenContext.context { + addAppLogEvent(postbox: context.account.postbox, type: "premium.promo_screen_fail") + } + + let errorText = presentationData.strings.Premium_Purchase_ErrorUnknown + let alertController = textAlertController(sharedContext: self.screenContext.sharedContext, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) + self.present(alertController) } + }, completed: { [weak self] in + guard let self else { + return + } + if let context = self.screenContext.context { + let _ = updatePremiumPromoConfigurationOnce(account: context.account).start() + } + self.inProgress = false + self.updateInProgress(false) + + self.isPremium = true + self.justBought = true + + self.updated(transition: .easeInOut(duration: 0.25)) + self.completion() + })) + } + }, error: { [weak self] error in + guard let self else { + return + } + self.inProgress = false + self.updateInProgress(false) + self.updated(transition: .immediate) + + var errorText: String? + switch error { + case .generic: + errorText = presentationData.strings.Premium_Purchase_ErrorUnknown + case .network: + errorText = presentationData.strings.Premium_Purchase_ErrorNetwork + case .notAllowed: + errorText = presentationData.strings.Premium_Purchase_ErrorNotAllowed + case .cantMakePayments: + errorText = presentationData.strings.Premium_Purchase_ErrorCantMakePayments + case .assignFailed: + errorText = presentationData.strings.Premium_Purchase_ErrorUnknown + case .tryLater: + errorText = presentationData.strings.Premium_Purchase_ErrorUnknown + case .cancelled: + break + } + + if let errorText = errorText { + if let context = self.screenContext.context { + addAppLogEvent(postbox: context.account.postbox, type: "premium.promo_screen_fail") } - })) - } else { - strongSelf.inProgress = false - strongSelf.updateInProgress(false) - strongSelf.updated(transition: .immediate) - } + + let alertController = textAlertController(sharedContext: self.screenContext.sharedContext, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) + self.present(alertController) + } + })) + } else { + self.inProgress = false + self.updateInProgress(false) + self.updated(transition: .immediate) } }) } @@ -3201,7 +3292,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { } func makeState() -> State { - return State(context: self.context, source: self.source, forceHasPremium: self.forceHasPremium, updateInProgress: self.updateInProgress, present: self.present, completion: self.completion) + return State(screenContext: self.screenContext, source: self.source, forceHasPremium: self.forceHasPremium, updateInProgress: self.updateInProgress, present: self.present, completion: self.completion) } static var body: Body { @@ -3248,12 +3339,12 @@ private final class PremiumIntroScreenComponent: CombinedComponent { availableSize: CGSize(width: min(414.0, context.availableSize.width), height: 220.0), transition: context.transition ) - } else if case let .emojiStatus(_, fileId, _, _) = context.component.source { + } else if case let .emojiStatus(_, fileId, _, _) = context.component.source, case let .accountContext(accountContext) = context.component.screenContext { header = emoji.update( component: EmojiHeaderComponent( - context: context.component.context, - animationCache: state.animationCache, - animationRenderer: state.animationRenderer, + context: accountContext, + animationCache: accountContext.animationCache, + animationRenderer: accountContext.animationRenderer, placeholderColor: environment.theme.list.mediaPlaceholderColor, accentColor: environment.theme.list.itemAccentColor, fileId: fileId, @@ -3356,7 +3447,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { } else if case .profile = context.component.source { secondaryTitleText = environment.strings.Premium_PersonalTitle(otherPeerName).string } else if case let .gift(fromPeerId, _, duration, _) = context.component.source { - if fromPeerId == context.component.context.account.peerId { + if case let .accountContext(accountContext) = context.component.screenContext, fromPeerId == accountContext.account.peerId { if duration == 12 { secondaryTitleText = environment.strings.Premium_GiftedTitleYou_12Month(otherPeerName).string } else if duration == 6 { @@ -3409,14 +3500,13 @@ private final class PremiumIntroScreenComponent: CombinedComponent { secondaryAttributedText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: emojiFile.fileId.id, file: emojiFile), range: range) } } - let accountContext = context.component.context + let presentController = context.component.present - let secondaryTitle = secondaryTitle.update( component: MultilineTextWithEntitiesComponent( - context: context.component.context, - animationCache: context.state.animationCache, - animationRenderer: context.state.animationRenderer, + context: context.component.screenContext.context, + animationCache: context.component.screenContext.context?.animationCache, + animationRenderer: context.component.screenContext.context?.animationRenderer, placeholderColor: environment.theme.list.mediaPlaceholderColor, text: .plain(secondaryAttributedText), horizontalAlignment: .center, @@ -3431,7 +3521,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { } } : nil, tapAction: { [weak state, weak environment] _, _ in - if let emojiFile = state?.emojiFile, let controller = environment?.controller() as? PremiumIntroScreen, let navigationController = controller.navigationController as? NavigationController { + if let emojiFile = state?.emojiFile, let controller = environment?.controller() as? PremiumIntroScreen, let context = controller.context, let navigationController = controller.navigationController as? NavigationController { for attribute in emojiFile.attributes { if case let .CustomEmoji(_, _, _, packReference) = attribute, let packReference = packReference { var loadedPack: LoadedStickerPack? @@ -3439,7 +3529,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { loadedPack = .result(info: info, items: items, installed: updatedInstalled ?? installed) } - let controller = accountContext.sharedContext.makeStickerPackScreen(context: accountContext, updatedPresentationData: nil, mainStickerPack: packReference, stickerPacks: [packReference], loadedStickerPacks: loadedPack.flatMap { [$0] } ?? [], actionTitle: nil, isEditing: false, expandIfNeeded: false, parentNavigationController: navigationController, sendSticker: { _, _, _ in + let controller = context.sharedContext.makeStickerPackScreen(context: context, updatedPresentationData: nil, mainStickerPack: packReference, stickerPacks: [packReference], loadedStickerPacks: loadedPack.flatMap { [$0] } ?? [], actionTitle: nil, isEditing: false, expandIfNeeded: false, parentNavigationController: navigationController, sendSticker: { _, _, _ in return false }, actionPerformed: { added in updatedInstalled = added @@ -3462,7 +3552,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { let scrollContent = scrollContent.update( component: ScrollComponent( content: AnyComponent(PremiumIntroScreenContentComponent( - context: context.component.context, + screenContext: context.component.screenContext, mode: context.component.mode, source: context.component.source, forceDark: context.component.forceDark, @@ -3570,8 +3660,8 @@ private final class PremiumIntroScreenComponent: CombinedComponent { ) var isUnusedGift = false - if case let .gift(fromId, _, _, giftCode) = context.component.source { - if let giftCode, giftCode.usedDate == nil, fromId != context.component.context.account.peerId { + if case let .gift(fromId, _, _, giftCode) = context.component.source, let accountContext = context.component.screenContext.context { + if let giftCode, giftCode.usedDate == nil, fromId != accountContext.account.peerId { isUnusedGift = true } } @@ -3589,7 +3679,11 @@ private final class PremiumIntroScreenComponent: CombinedComponent { if !buttonIsHidden { let buttonTitle: String - if isUnusedGift { + var buttonSubtitle: String? + if case let .auth(price) = context.component.source { + buttonTitle = "Sign up for \(price)" + buttonSubtitle = "Get Telegram Premium for 1 week" + } else if isUnusedGift { buttonTitle = environment.strings.Premium_Gift_ApplyLink } else if state.isPremium == true && state.canUpgrade { buttonTitle = state.isAnnual ? environment.strings.Premium_UpgradeForAnnual(state.price ?? "—").string : environment.strings.Premium_UpgradeFor(state.price ?? "—").string @@ -3597,10 +3691,12 @@ private final class PremiumIntroScreenComponent: CombinedComponent { buttonTitle = state.isAnnual ? environment.strings.Premium_SubscribeForAnnual(state.price ?? "—").string : environment.strings.Premium_SubscribeFor(state.price ?? "—").string } + let controller = environment.controller let sideInset: CGFloat = 16.0 let button = button.update( component: SolidRoundedButtonComponent( title: buttonTitle, + subtitle: buttonSubtitle, theme: SolidRoundedButtonComponent.Theme( backgroundColor: UIColor(rgb: 0x8878ff), backgroundColors: [ @@ -3616,7 +3712,12 @@ private final class PremiumIntroScreenComponent: CombinedComponent { gloss: true, isLoading: state.inProgress, action: { - state.buy() + if let controller = controller() as? PremiumIntroScreen, let customProceed = controller.customProceed { + controller.dismiss() + customProceed() + } else { + state.buy() + } } ), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - environment.safeInsets.left - environment.safeInsets.right, height: 50.0), @@ -3691,12 +3792,70 @@ private final class PremiumIntroScreenComponent: CombinedComponent { } public final class PremiumIntroScreen: ViewControllerComponentContainer { + public enum ScreenContext { + case accountContext(AccountContext) + case sharedContext(SharedAccountContext, TelegramEngineUnauthorized, InAppPurchaseManager) + + var context: AccountContext? { + switch self { + case let .accountContext(context): + return context + case .sharedContext: + return nil + } + } + + var sharedContext: SharedAccountContext { + switch self { + case let .accountContext(context): + return context.sharedContext + case let .sharedContext(sharedContext, _, _): + return sharedContext + } + } + + var inAppPurchaseManager: InAppPurchaseManager? { + switch self { + case let .accountContext(context): + return context.inAppPurchaseManager + case let .sharedContext(_, _, inAppPurchaseManager): + return inAppPurchaseManager + } + } + + var presentationData: PresentationData { + switch self { + case let .accountContext(context): + return context.sharedContext.currentPresentationData.with { $0 } + case let .sharedContext(sharedContext, _, _): + return sharedContext.currentPresentationData.with { $0 } + } + } + + var updatedPresentationData: (initial: PresentationData, signal: Signal) { + switch self { + case let .accountContext(context): + return (initial: context.sharedContext.currentPresentationData.with { $0 }, signal: context.sharedContext.presentationData) + case let .sharedContext(sharedContext, _, _): + return (initial: sharedContext.currentPresentationData.with { $0 }, signal: sharedContext.presentationData) + } + } + } + public enum Mode { case premium case business } - fileprivate let context: AccountContext + fileprivate var context: AccountContext? { + switch self.screenContext { + case let .accountContext(context): + return context + case .sharedContext: + return nil + } + } + private let screenContext: ScreenContext fileprivate let mode: Mode private var didSetReady = false @@ -3706,21 +3865,28 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer { } public weak var sourceView: UIView? + public var sourceRect: CGRect? public weak var containerView: UIView? public var animationColor: UIColor? - public init(context: AccountContext, mode: Mode = .premium, source: PremiumSource, modal: Bool = true, forceDark: Bool = false, forceHasPremium: Bool = false) { - self.context = context + public convenience init(context: AccountContext, mode: Mode = .premium, source: PremiumSource, modal: Bool = true, forceDark: Bool = false, forceHasPremium: Bool = false) { + self.init(screenContext: .accountContext(context), mode: mode, source: source, modal: modal, forceDark: forceDark, forceHasPremium: forceHasPremium) + } + + public init(screenContext: ScreenContext, mode: Mode = .premium, source: PremiumSource, modal: Bool = true, forceDark: Bool = false, forceHasPremium: Bool = false) { + self.screenContext = screenContext self.mode = mode - + + let presentationData = screenContext.presentationData + var updateInProgressImpl: ((Bool) -> Void)? var pushImpl: ((ViewController) -> Void)? var presentImpl: ((ViewController) -> Void)? var completionImpl: (() -> Void)? var copyLinkImpl: ((String) -> Void)? var shareLinkImpl: ((String) -> Void)? - super.init(context: context, component: PremiumIntroScreenComponent( - context: context, + super.init(component: PremiumIntroScreenComponent( + screenContext: screenContext, mode: mode, source: source, forceDark: forceDark, @@ -3743,10 +3909,8 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer { shareLink: { link in shareLinkImpl?(link) } - ), navigationBarAppearance: .transparent, presentationMode: modal ? .modal : .default, theme: forceDark ? .dark : .default) - - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - + ), navigationBarAppearance: .transparent, presentationMode: modal ? .modal : .default, theme: forceDark ? .dark : .default, updatedPresentationData: screenContext.updatedPresentationData) + if modal { let cancelItem = UIBarButtonItem(title: presentationData.strings.Common_Close, style: .plain, target: self, action: #selector(self.cancelPressed)) self.navigationItem.setLeftBarButton(cancelItem, animated: false) @@ -3756,11 +3920,12 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer { } updateInProgressImpl = { [weak self] inProgress in - if let strongSelf = self { - strongSelf.navigationItem.leftBarButtonItem?.isEnabled = !inProgress - strongSelf.view.disablesInteractiveTransitionGestureRecognizer = inProgress - strongSelf.view.disablesInteractiveModalDismiss = inProgress + guard let self else { + return } + self.navigationItem.leftBarButtonItem?.isEnabled = !inProgress + self.view.disablesInteractiveTransitionGestureRecognizer = inProgress + self.view.disablesInteractiveModalDismiss = inProgress } presentImpl = { [weak self] c in @@ -3789,12 +3954,11 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer { } self.dismissAllTooltips() - let presentationData = context.sharedContext.currentPresentationData.with { $0 } self.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, position: .top, action: { _ in return true }), in: .current) } shareLinkImpl = { [weak self] link in - guard let self, let navigationController = self.navigationController as? NavigationController else { + guard let self, case let .accountContext(context) = screenContext, let navigationController = self.navigationController as? NavigationController else { return } @@ -3807,7 +3971,6 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer { HapticFeedback().success() } - let presentationData = context.sharedContext.currentPresentationData.with { $0 } (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) @@ -3820,7 +3983,7 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer { navigationController.pushViewController(peerSelectionController) } - if case .business = mode { + if case .business = mode, case let .accountContext(context) = screenContext { context.account.viewTracker.keepQuickRepliesApproximatelyUpdated() context.account.viewTracker.keepBusinessLinksApproximatelyUpdated() } @@ -3884,6 +4047,7 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer { if let sourceView = self.sourceView { view.animateFrom = sourceView + view.sourceRect = self.sourceRect view.containerView = self.containerView view.animateIn() diff --git a/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift b/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift index bbf34ff84e..e686644d5c 100644 --- a/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift @@ -16,6 +16,7 @@ import BundleIconComponent import Markdown import SolidRoundedButtonNode import BlurredBackgroundComponent +import PremiumCoinComponent public class PremiumLimitsListScreen: ViewController { final class Node: ViewControllerTracingNode, ASScrollViewDelegate, ASGestureRecognizerDelegate { diff --git a/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift b/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift index e18a0ac730..7b724ca498 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift @@ -315,7 +315,7 @@ private func selectivePrivacyPeersControllerEntries(presentationData: Presentati entries.append(.footerItem(footer)) } - if !peers.isEmpty { + if !peers.isEmpty || state.enableForPremium || state.enableForBots { entries.append(.deleteItem(presentationData.strings.Privacy_Exceptions_DeleteAllExceptions)) } diff --git a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift index cdddb1136c..09079638a9 100644 --- a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift +++ b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift @@ -233,6 +233,7 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, ASScrollView }, openWebApp: { _ in }, openPhotoSetup: { }, openAdInfo: { _ in + }, openAccountFreezeInfo: { }) let chatListPresentationData = ChatListPresentationData(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true) diff --git a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift index b93df4e10c..f2ae983894 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift @@ -382,6 +382,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, ASScrollViewDelegate { }, openWebApp: { _ in }, openPhotoSetup: { }, openAdInfo: { _ in + }, openAccountFreezeInfo: { }) func makeChatListItem( diff --git a/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift b/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift index 5574b7f9f1..3c10b7dca4 100644 --- a/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift +++ b/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift @@ -1412,7 +1412,7 @@ public final class SolidRoundedButtonView: UIView { } self.titleNode.attributedText = titleText - self.subtitleNode.attributedText = NSAttributedString(string: self.subtitle ?? "", font: Font.regular(14.0), textColor: theme.foregroundColor) + self.subtitleNode.attributedText = NSAttributedString(string: self.subtitle ?? "", font: Font.medium(11.0), textColor: theme.foregroundColor.withAlphaComponent(0.7)) self.iconNode.image = generateTintedImage(image: self.iconNode.image, color: theme.foregroundColor) @@ -1472,7 +1472,7 @@ public final class SolidRoundedButtonView: UIView { } let titleSize = self.titleNode.updateLayout(buttonSize) - let spacingOffset: CGFloat = 9.0 + let spacingOffset: CGFloat = 7.0 let verticalInset: CGFloat = self.subtitle == nil ? floor((buttonFrame.height - titleSize.height) / 2.0) : floor((buttonFrame.height - titleSize.height) / 2.0) - spacingOffset let iconSpacing: CGFloat = self.iconSpacing let badgeSpacing: CGFloat = 6.0 @@ -1533,11 +1533,11 @@ public final class SolidRoundedButtonView: UIView { } if self.subtitle != self.subtitleNode.attributedText?.string { - self.subtitleNode.attributedText = NSAttributedString(string: self.subtitle ?? "", font: Font.regular(14.0), textColor: self.theme.foregroundColor) + self.subtitleNode.attributedText = NSAttributedString(string: self.subtitle ?? "", font: Font.medium(11.0), textColor: self.theme.foregroundColor.withAlphaComponent(0.7)) } let subtitleSize = self.subtitleNode.updateLayout(buttonSize) - let subtitleFrame = CGRect(origin: CGPoint(x: buttonFrame.minX + floor((buttonFrame.width - subtitleSize.width) / 2.0), y: buttonFrame.minY + floor((buttonFrame.height - titleSize.height) / 2.0) + spacingOffset + 2.0), size: subtitleSize) + let subtitleFrame = CGRect(origin: CGPoint(x: buttonFrame.minX + floor((buttonFrame.width - subtitleSize.width) / 2.0), y: buttonFrame.minY + floor((buttonFrame.height - titleSize.height) / 2.0) + spacingOffset + 7.0), size: subtitleSize) transition.updateFrame(view: self.subtitleNode, frame: subtitleFrame) if previousSubtitle == nil && self.subtitle != nil { diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 06fbfc611d..0efc2bec9d 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -84,7 +84,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1778593322] = { return Api.BotApp.parse_botApp($0) } dict[1571189943] = { return Api.BotApp.parse_botAppNotModified($0) } dict[-912582320] = { return Api.BotAppSettings.parse_botAppSettings($0) } - dict[-1989921868] = { return Api.BotBusinessConnection.parse_botBusinessConnection($0) } + dict[-1892371723] = { return Api.BotBusinessConnection.parse_botBusinessConnection($0) } dict[-1032140601] = { return Api.BotCommand.parse_botCommand($0) } dict[-1180016534] = { return Api.BotCommandScope.parse_botCommandScopeChatAdmins($0) } dict[1877059713] = { return Api.BotCommandScope.parse_botCommandScopeChats($0) } @@ -118,6 +118,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-867328308] = { return Api.BusinessAwayMessageSchedule.parse_businessAwayMessageScheduleCustom($0) } dict[-1007487743] = { return Api.BusinessAwayMessageSchedule.parse_businessAwayMessageScheduleOutsideWorkHours($0) } dict[-1198722189] = { return Api.BusinessBotRecipients.parse_businessBotRecipients($0) } + dict[-1604170505] = { return Api.BusinessBotRights.parse_businessBotRights($0) } dict[-1263638929] = { return Api.BusinessChatLink.parse_businessChatLink($0) } dict[-451302485] = { return Api.BusinessGreetingMessage.parse_businessGreetingMessage($0) } dict[1510606445] = { return Api.BusinessIntro.parse_businessIntro($0) } @@ -224,7 +225,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1713193015] = { return Api.ChatReactions.parse_chatReactionsSome($0) } dict[-1390068360] = { return Api.CodeSettings.parse_codeSettings($0) } dict[-870702050] = { return Api.Config.parse_config($0) } - dict[-1123645951] = { return Api.ConnectedBot.parse_connectedBot($0) } + dict[-849058964] = { return Api.ConnectedBot.parse_connectedBot($0) } dict[429997937] = { return Api.ConnectedBotStarRef.parse_connectedBotStarRef($0) } dict[341499403] = { return Api.Contact.parse_contact($0) } dict[496600883] = { return Api.ContactBirthday.parse_contactBirthday($0) } @@ -480,6 +481,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[853188252] = { return Api.InputStickerSetItem.parse_inputStickerSetItem($0) } dict[70813275] = { return Api.InputStickeredMedia.parse_inputStickeredMediaDocument($0) } dict[1251549527] = { return Api.InputStickeredMedia.parse_inputStickeredMediaPhoto($0) } + dict[-1682807955] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentAuthCode($0) } dict[1634697192] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentGiftPremium($0) } dict[-75955309] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentPremiumGiftCode($0) } dict[369444042] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentPremiumGiveaway($0) } @@ -575,6 +577,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1281329567] = { return Api.MessageAction.parse_messageActionGroupCallScheduled($0) } dict[-1615153660] = { return Api.MessageAction.parse_messageActionHistoryClear($0) } dict[1345295095] = { return Api.MessageAction.parse_messageActionInviteToGroupCall($0) } + dict[-1126755303] = { return Api.MessageAction.parse_messageActionPaidMessagesPrice($0) } + dict[-1407246387] = { return Api.MessageAction.parse_messageActionPaidMessagesRefunded($0) } dict[1102307842] = { return Api.MessageAction.parse_messageActionPaymentRefunded($0) } dict[-970673810] = { return Api.MessageAction.parse_messageActionPaymentSent($0) } dict[-6288180] = { return Api.MessageAction.parse_messageActionPaymentSentMe($0) } @@ -1110,6 +1114,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1821035490] = { return Api.Update.parse_updateSavedGifs($0) } dict[969307186] = { return Api.Update.parse_updateSavedReactionTags($0) } dict[1960361625] = { return Api.Update.parse_updateSavedRingtones($0) } + dict[1347068303] = { return Api.Update.parse_updateSentPhoneCode($0) } dict[2103604867] = { return Api.Update.parse_updateSentStoryReaction($0) } dict[-337352679] = { return Api.Update.parse_updateServiceNotification($0) } dict[-245208620] = { return Api.Update.parse_updateSmsJob($0) } @@ -1217,6 +1222,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[957176926] = { return Api.auth.LoginToken.parse_loginTokenSuccess($0) } dict[326715557] = { return Api.auth.PasswordRecovery.parse_passwordRecovery($0) } dict[1577067778] = { return Api.auth.SentCode.parse_sentCode($0) } + dict[-674301568] = { return Api.auth.SentCode.parse_sentCodePaymentRequired($0) } dict[596704836] = { return Api.auth.SentCode.parse_sentCodeSuccess($0) } dict[1035688326] = { return Api.auth.SentCodeType.parse_sentCodeTypeApp($0) } dict[1398007207] = { return Api.auth.SentCodeType.parse_sentCodeTypeCall($0) } @@ -1589,6 +1595,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.BusinessBotRecipients: _1.serialize(buffer, boxed) + case let _1 as Api.BusinessBotRights: + _1.serialize(buffer, boxed) case let _1 as Api.BusinessChatLink: _1.serialize(buffer, boxed) case let _1 as Api.BusinessGreetingMessage: diff --git a/submodules/TelegramApi/Sources/Api1.swift b/submodules/TelegramApi/Sources/Api1.swift index 04b97a79c0..f6e3fd0d8a 100644 --- a/submodules/TelegramApi/Sources/Api1.swift +++ b/submodules/TelegramApi/Sources/Api1.swift @@ -1170,27 +1170,28 @@ public extension Api { } public extension Api { enum BotBusinessConnection: TypeConstructorDescription { - case botBusinessConnection(flags: Int32, connectionId: String, userId: Int64, dcId: Int32, date: Int32) + case botBusinessConnection(flags: Int32, connectionId: String, userId: Int64, dcId: Int32, date: Int32, rights: Api.BusinessBotRights?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .botBusinessConnection(let flags, let connectionId, let userId, let dcId, let date): + case .botBusinessConnection(let flags, let connectionId, let userId, let dcId, let date, let rights): if boxed { - buffer.appendInt32(-1989921868) + buffer.appendInt32(-1892371723) } serializeInt32(flags, buffer: buffer, boxed: false) serializeString(connectionId, buffer: buffer, boxed: false) serializeInt64(userId, buffer: buffer, boxed: false) serializeInt32(dcId, buffer: buffer, boxed: false) serializeInt32(date, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 2) != 0 {rights!.serialize(buffer, true)} break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .botBusinessConnection(let flags, let connectionId, let userId, let dcId, let date): - return ("botBusinessConnection", [("flags", flags as Any), ("connectionId", connectionId as Any), ("userId", userId as Any), ("dcId", dcId as Any), ("date", date as Any)]) + case .botBusinessConnection(let flags, let connectionId, let userId, let dcId, let date, let rights): + return ("botBusinessConnection", [("flags", flags as Any), ("connectionId", connectionId as Any), ("userId", userId as Any), ("dcId", dcId as Any), ("date", date as Any), ("rights", rights as Any)]) } } @@ -1205,13 +1206,18 @@ public extension Api { _4 = reader.readInt32() var _5: Int32? _5 = reader.readInt32() + var _6: Api.BusinessBotRights? + if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { + _6 = Api.parse(reader, signature: signature) as? Api.BusinessBotRights + } } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return Api.BotBusinessConnection.botBusinessConnection(flags: _1!, connectionId: _2!, userId: _3!, dcId: _4!, date: _5!) + let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.BotBusinessConnection.botBusinessConnection(flags: _1!, connectionId: _2!, userId: _3!, dcId: _4!, date: _5!, rights: _6) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api13.swift b/submodules/TelegramApi/Sources/Api13.swift index 08c3149890..d0c6d7ee81 100644 --- a/submodules/TelegramApi/Sources/Api13.swift +++ b/submodules/TelegramApi/Sources/Api13.swift @@ -116,6 +116,7 @@ public extension Api { } public extension Api { indirect enum InputStorePaymentPurpose: TypeConstructorDescription { + case inputStorePaymentAuthCode(flags: Int32, phoneNumber: String, phoneCodeHash: String, currency: String, amount: Int64) case inputStorePaymentGiftPremium(userId: Api.InputUser, currency: String, amount: Int64) case inputStorePaymentPremiumGiftCode(flags: Int32, users: [Api.InputUser], boostPeer: Api.InputPeer?, currency: String, amount: Int64, message: Api.TextWithEntities?) case inputStorePaymentPremiumGiveaway(flags: Int32, boostPeer: Api.InputPeer, additionalPeers: [Api.InputPeer]?, countriesIso2: [String]?, prizeDescription: String?, randomId: Int64, untilDate: Int32, currency: String, amount: Int64) @@ -126,6 +127,16 @@ public extension Api { public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { + case .inputStorePaymentAuthCode(let flags, let phoneNumber, let phoneCodeHash, let currency, let amount): + if boxed { + buffer.appendInt32(-1682807955) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeString(phoneNumber, buffer: buffer, boxed: false) + serializeString(phoneCodeHash, buffer: buffer, boxed: false) + serializeString(currency, buffer: buffer, boxed: false) + serializeInt64(amount, buffer: buffer, boxed: false) + break case .inputStorePaymentGiftPremium(let userId, let currency, let amount): if boxed { buffer.appendInt32(1634697192) @@ -223,6 +234,8 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { + case .inputStorePaymentAuthCode(let flags, let phoneNumber, let phoneCodeHash, let currency, let amount): + return ("inputStorePaymentAuthCode", [("flags", flags as Any), ("phoneNumber", phoneNumber as Any), ("phoneCodeHash", phoneCodeHash as Any), ("currency", currency as Any), ("amount", amount as Any)]) case .inputStorePaymentGiftPremium(let userId, let currency, let amount): return ("inputStorePaymentGiftPremium", [("userId", userId as Any), ("currency", currency as Any), ("amount", amount as Any)]) case .inputStorePaymentPremiumGiftCode(let flags, let users, let boostPeer, let currency, let amount, let message): @@ -240,6 +253,29 @@ public extension Api { } } + public static func parse_inputStorePaymentAuthCode(_ reader: BufferReader) -> InputStorePaymentPurpose? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: String? + _3 = parseString(reader) + var _4: String? + _4 = parseString(reader) + var _5: Int64? + _5 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.InputStorePaymentPurpose.inputStorePaymentAuthCode(flags: _1!, phoneNumber: _2!, phoneCodeHash: _3!, currency: _4!, amount: _5!) + } + else { + return nil + } + } public static func parse_inputStorePaymentGiftPremium(_ reader: BufferReader) -> InputStorePaymentPurpose? { var _1: Api.InputUser? if let signature = reader.readInt32() { diff --git a/submodules/TelegramApi/Sources/Api15.swift b/submodules/TelegramApi/Sources/Api15.swift index 72842970c5..63df242808 100644 --- a/submodules/TelegramApi/Sources/Api15.swift +++ b/submodules/TelegramApi/Sources/Api15.swift @@ -363,6 +363,8 @@ public extension Api { case messageActionGroupCallScheduled(call: Api.InputGroupCall, scheduleDate: Int32) case messageActionHistoryClear case messageActionInviteToGroupCall(call: Api.InputGroupCall, users: [Int64]) + case messageActionPaidMessagesPrice(stars: Int64) + case messageActionPaidMessagesRefunded(count: Int32, stars: Int64) case messageActionPaymentRefunded(flags: Int32, peer: Api.Peer, currency: String, totalAmount: Int64, payload: Buffer?, charge: Api.PaymentCharge) case messageActionPaymentSent(flags: Int32, currency: String, totalAmount: Int64, invoiceSlug: String?, subscriptionUntilDate: Int32?) case messageActionPaymentSentMe(flags: Int32, currency: String, totalAmount: Int64, payload: Buffer, info: Api.PaymentRequestedInfo?, shippingOptionId: String?, charge: Api.PaymentCharge, subscriptionUntilDate: Int32?) @@ -595,6 +597,19 @@ public extension Api { serializeInt64(item, buffer: buffer, boxed: false) } break + case .messageActionPaidMessagesPrice(let stars): + if boxed { + buffer.appendInt32(-1126755303) + } + serializeInt64(stars, buffer: buffer, boxed: false) + break + case .messageActionPaidMessagesRefunded(let count, let stars): + if boxed { + buffer.appendInt32(-1407246387) + } + serializeInt32(count, buffer: buffer, boxed: false) + serializeInt64(stars, buffer: buffer, boxed: false) + break case .messageActionPaymentRefunded(let flags, let peer, let currency, let totalAmount, let payload, let charge): if boxed { buffer.appendInt32(1102307842) @@ -847,6 +862,10 @@ public extension Api { return ("messageActionHistoryClear", []) case .messageActionInviteToGroupCall(let call, let users): return ("messageActionInviteToGroupCall", [("call", call as Any), ("users", users as Any)]) + case .messageActionPaidMessagesPrice(let stars): + return ("messageActionPaidMessagesPrice", [("stars", stars as Any)]) + case .messageActionPaidMessagesRefunded(let count, let stars): + return ("messageActionPaidMessagesRefunded", [("count", count as Any), ("stars", stars as Any)]) case .messageActionPaymentRefunded(let flags, let peer, let currency, let totalAmount, let payload, let charge): return ("messageActionPaymentRefunded", [("flags", flags as Any), ("peer", peer as Any), ("currency", currency as Any), ("totalAmount", totalAmount as Any), ("payload", payload as Any), ("charge", charge as Any)]) case .messageActionPaymentSent(let flags, let currency, let totalAmount, let invoiceSlug, let subscriptionUntilDate): @@ -1277,6 +1296,31 @@ public extension Api { return nil } } + public static func parse_messageActionPaidMessagesPrice(_ reader: BufferReader) -> MessageAction? { + var _1: Int64? + _1 = reader.readInt64() + let _c1 = _1 != nil + if _c1 { + return Api.MessageAction.messageActionPaidMessagesPrice(stars: _1!) + } + else { + return nil + } + } + public static func parse_messageActionPaidMessagesRefunded(_ reader: BufferReader) -> MessageAction? { + 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.MessageAction.messageActionPaidMessagesRefunded(count: _1!, stars: _2!) + } + else { + return nil + } + } public static func parse_messageActionPaymentRefunded(_ reader: BufferReader) -> MessageAction? { var _1: Int32? _1 = reader.readInt32() diff --git a/submodules/TelegramApi/Sources/Api26.swift b/submodules/TelegramApi/Sources/Api26.swift index 4d6ba7fbaf..3947b37442 100644 --- a/submodules/TelegramApi/Sources/Api26.swift +++ b/submodules/TelegramApi/Sources/Api26.swift @@ -1103,6 +1103,7 @@ public extension Api { case updateSavedGifs case updateSavedReactionTags case updateSavedRingtones + case updateSentPhoneCode(sentCode: Api.auth.SentCode) case updateSentStoryReaction(peer: Api.Peer, storyId: Int32, reaction: Api.Reaction) case updateServiceNotification(flags: Int32, inboxDate: Int32?, type: String, message: String, media: Api.MessageMedia, entities: [Api.MessageEntity]) case updateSmsJob(jobId: String) @@ -2198,6 +2199,12 @@ public extension Api { buffer.appendInt32(1960361625) } + break + case .updateSentPhoneCode(let sentCode): + if boxed { + buffer.appendInt32(1347068303) + } + sentCode.serialize(buffer, true) break case .updateSentStoryReaction(let peer, let storyId, let reaction): if boxed { @@ -2602,6 +2609,8 @@ public extension Api { return ("updateSavedReactionTags", []) case .updateSavedRingtones: return ("updateSavedRingtones", []) + case .updateSentPhoneCode(let sentCode): + return ("updateSentPhoneCode", [("sentCode", sentCode as Any)]) case .updateSentStoryReaction(let peer, let storyId, let reaction): return ("updateSentStoryReaction", [("peer", peer as Any), ("storyId", storyId as Any), ("reaction", reaction as Any)]) case .updateServiceNotification(let flags, let inboxDate, let type, let message, let media, let entities): @@ -4803,6 +4812,19 @@ public extension Api { public static func parse_updateSavedRingtones(_ reader: BufferReader) -> Update? { return Api.Update.updateSavedRingtones } + public static func parse_updateSentPhoneCode(_ reader: BufferReader) -> Update? { + var _1: Api.auth.SentCode? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.auth.SentCode + } + let _c1 = _1 != nil + if _c1 { + return Api.Update.updateSentPhoneCode(sentCode: _1!) + } + else { + return nil + } + } public static func parse_updateSentStoryReaction(_ reader: BufferReader) -> Update? { var _1: Api.Peer? if let signature = reader.readInt32() { diff --git a/submodules/TelegramApi/Sources/Api29.swift b/submodules/TelegramApi/Sources/Api29.swift index a63853f7e2..fe6078fe6d 100644 --- a/submodules/TelegramApi/Sources/Api29.swift +++ b/submodules/TelegramApi/Sources/Api29.swift @@ -567,6 +567,7 @@ public extension Api.auth { public extension Api.auth { enum SentCode: TypeConstructorDescription { case sentCode(flags: Int32, type: Api.auth.SentCodeType, phoneCodeHash: String, nextType: Api.auth.CodeType?, timeout: Int32?) + case sentCodePaymentRequired(storeProduct: String, phoneCodeHash: String) case sentCodeSuccess(authorization: Api.auth.Authorization) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { @@ -581,6 +582,13 @@ public extension Api.auth { if Int(flags) & Int(1 << 1) != 0 {nextType!.serialize(buffer, true)} if Int(flags) & Int(1 << 2) != 0 {serializeInt32(timeout!, buffer: buffer, boxed: false)} break + case .sentCodePaymentRequired(let storeProduct, let phoneCodeHash): + if boxed { + buffer.appendInt32(-674301568) + } + serializeString(storeProduct, buffer: buffer, boxed: false) + serializeString(phoneCodeHash, buffer: buffer, boxed: false) + break case .sentCodeSuccess(let authorization): if boxed { buffer.appendInt32(596704836) @@ -594,6 +602,8 @@ public extension Api.auth { switch self { case .sentCode(let flags, let type, let phoneCodeHash, let nextType, let timeout): return ("sentCode", [("flags", flags as Any), ("type", type as Any), ("phoneCodeHash", phoneCodeHash as Any), ("nextType", nextType as Any), ("timeout", timeout as Any)]) + case .sentCodePaymentRequired(let storeProduct, let phoneCodeHash): + return ("sentCodePaymentRequired", [("storeProduct", storeProduct as Any), ("phoneCodeHash", phoneCodeHash as Any)]) case .sentCodeSuccess(let authorization): return ("sentCodeSuccess", [("authorization", authorization as Any)]) } @@ -626,6 +636,20 @@ public extension Api.auth { return nil } } + public static func parse_sentCodePaymentRequired(_ reader: BufferReader) -> SentCode? { + var _1: String? + _1 = parseString(reader) + var _2: String? + _2 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.auth.SentCode.sentCodePaymentRequired(storeProduct: _1!, phoneCodeHash: _2!) + } + else { + return nil + } + } public static func parse_sentCodeSuccess(_ reader: BufferReader) -> SentCode? { var _1: Api.auth.Authorization? if let signature = reader.readInt32() { diff --git a/submodules/TelegramApi/Sources/Api3.swift b/submodules/TelegramApi/Sources/Api3.swift index 57f473f326..b6ec88ab5b 100644 --- a/submodules/TelegramApi/Sources/Api3.swift +++ b/submodules/TelegramApi/Sources/Api3.swift @@ -54,6 +54,42 @@ public extension Api { } } +public extension Api { + enum BusinessBotRights: TypeConstructorDescription { + case businessBotRights(flags: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .businessBotRights(let flags): + if boxed { + buffer.appendInt32(-1604170505) + } + serializeInt32(flags, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .businessBotRights(let flags): + return ("businessBotRights", [("flags", flags as Any)]) + } + } + + public static func parse_businessBotRights(_ reader: BufferReader) -> BusinessBotRights? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return Api.BusinessBotRights.businessBotRights(flags: _1!) + } + else { + return nil + } + } + + } +} public extension Api { enum BusinessChatLink: TypeConstructorDescription { case businessChatLink(flags: Int32, link: String, message: String, entities: [Api.MessageEntity]?, title: String?, views: Int32) diff --git a/submodules/TelegramApi/Sources/Api38.swift b/submodules/TelegramApi/Sources/Api38.swift index 9c10a1bc0b..e2bf3f4406 100644 --- a/submodules/TelegramApi/Sources/Api38.swift +++ b/submodules/TelegramApi/Sources/Api38.swift @@ -1604,13 +1604,14 @@ public extension Api.functions.account { } } public extension Api.functions.account { - static func updateConnectedBot(flags: Int32, bot: Api.InputUser, recipients: Api.InputBusinessBotRecipients) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func updateConnectedBot(flags: Int32, rights: Api.BusinessBotRights?, bot: Api.InputUser, recipients: Api.InputBusinessBotRecipients) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(1138250269) + buffer.appendInt32(1721797758) serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {rights!.serialize(buffer, true)} bot.serialize(buffer, true) recipients.serialize(buffer, true) - return (FunctionDescription(name: "account.updateConnectedBot", parameters: [("flags", String(describing: flags)), ("bot", String(describing: bot)), ("recipients", String(describing: recipients))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + return (FunctionDescription(name: "account.updateConnectedBot", parameters: [("flags", String(describing: flags)), ("rights", String(describing: rights)), ("bot", String(describing: bot)), ("recipients", String(describing: recipients))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in let reader = BufferReader(buffer) var result: Api.Updates? if let signature = reader.readInt32() { @@ -9063,11 +9064,11 @@ public extension Api.functions.payments { } } public extension Api.functions.payments { - static func canPurchasePremium(purpose: Api.InputStorePaymentPurpose) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func canPurchaseStore(purpose: Api.InputStorePaymentPurpose) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(-1614700874) + buffer.appendInt32(1339842215) purpose.serialize(buffer, true) - return (FunctionDescription(name: "payments.canPurchasePremium", parameters: [("purpose", String(describing: purpose))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + return (FunctionDescription(name: "payments.canPurchaseStore", parameters: [("purpose", String(describing: purpose))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in let reader = BufferReader(buffer) var result: Api.Bool? if let signature = reader.readInt32() { diff --git a/submodules/TelegramApi/Sources/Api5.swift b/submodules/TelegramApi/Sources/Api5.swift index 609de6bbae..2f4236c0b7 100644 --- a/submodules/TelegramApi/Sources/Api5.swift +++ b/submodules/TelegramApi/Sources/Api5.swift @@ -664,25 +664,26 @@ public extension Api { } public extension Api { enum ConnectedBot: TypeConstructorDescription { - case connectedBot(flags: Int32, botId: Int64, recipients: Api.BusinessBotRecipients) + case connectedBot(flags: Int32, botId: Int64, recipients: Api.BusinessBotRecipients, rights: Api.BusinessBotRights) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .connectedBot(let flags, let botId, let recipients): + case .connectedBot(let flags, let botId, let recipients, let rights): if boxed { - buffer.appendInt32(-1123645951) + buffer.appendInt32(-849058964) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt64(botId, buffer: buffer, boxed: false) recipients.serialize(buffer, true) + rights.serialize(buffer, true) break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .connectedBot(let flags, let botId, let recipients): - return ("connectedBot", [("flags", flags as Any), ("botId", botId as Any), ("recipients", recipients as Any)]) + case .connectedBot(let flags, let botId, let recipients, let rights): + return ("connectedBot", [("flags", flags as Any), ("botId", botId as Any), ("recipients", recipients as Any), ("rights", rights as Any)]) } } @@ -695,11 +696,16 @@ public extension Api { if let signature = reader.readInt32() { _3 = Api.parse(reader, signature: signature) as? Api.BusinessBotRecipients } + var _4: Api.BusinessBotRights? + if let signature = reader.readInt32() { + _4 = Api.parse(reader, signature: signature) as? Api.BusinessBotRights + } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.ConnectedBot.connectedBot(flags: _1!, botId: _2!, recipients: _3!) + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.ConnectedBot.connectedBot(flags: _1!, botId: _2!, recipients: _3!, rights: _4!) } else { return nil diff --git a/submodules/TelegramCore/Sources/Account/Account.swift b/submodules/TelegramCore/Sources/Account/Account.swift index d1b1b90e71..3175e1573d 100644 --- a/submodules/TelegramCore/Sources/Account/Account.swift +++ b/submodules/TelegramCore/Sources/Account/Account.swift @@ -73,13 +73,13 @@ public class UnauthorizedAccount { public let testingEnvironment: Bool public let postbox: Postbox public let network: Network - private let stateManager: UnauthorizedAccountStateManager + let stateManager: UnauthorizedAccountStateManager private let updateLoginTokenPipe = ValuePipe() public var updateLoginTokenEvents: Signal { return self.updateLoginTokenPipe.signal() } - + private let serviceNotificationPipe = ValuePipe() public var serviceNotificationEvents: Signal { return self.serviceNotificationPipe.signal() @@ -91,7 +91,7 @@ public class UnauthorizedAccount { public let shouldBeServiceTaskMaster = Promise() - init(networkArguments: NetworkInitializationArguments, id: AccountRecordId, rootPath: String, basePath: String, testingEnvironment: Bool, postbox: Postbox, network: Network, shouldKeepAutoConnection: Bool = true) { + init(accountManager: AccountManager, networkArguments: NetworkInitializationArguments, id: AccountRecordId, rootPath: String, basePath: String, testingEnvironment: Bool, postbox: Postbox, network: Network, shouldKeepAutoConnection: Bool = true) { self.networkArguments = networkArguments self.id = id self.rootPath = rootPath @@ -101,11 +101,84 @@ public class UnauthorizedAccount { self.network = network let updateLoginTokenPipe = self.updateLoginTokenPipe let serviceNotificationPipe = self.serviceNotificationPipe - self.stateManager = UnauthorizedAccountStateManager(network: network, updateLoginToken: { - updateLoginTokenPipe.putNext(Void()) - }, displayServiceNotification: { text in - serviceNotificationPipe.putNext(text) - }) + let masterDatacenterId = Int32(network.mtProto.datacenterId) + + var updateSentCodeImpl: ((Api.auth.SentCode) -> Void)? + self.stateManager = UnauthorizedAccountStateManager( + network: network, + updateLoginToken: { + updateLoginTokenPipe.putNext(Void()) + }, + updateSentCode: { sentCode in + updateSentCodeImpl?(sentCode) + }, + displayServiceNotification: { text in + serviceNotificationPipe.putNext(text) + } + ) + + updateSentCodeImpl = { [weak self] sentCode in + switch sentCode { + case .sentCodePaymentRequired: + break + case let .sentCode(_, type, phoneCodeHash, nextType, codeTimeout): + let _ = postbox.transaction({ transaction in + var parsedNextType: AuthorizationCodeNextType? + if let nextType = nextType { + parsedNextType = AuthorizationCodeNextType(apiType: nextType) + } + if let state = transaction.getState() as? UnauthorizedAccountState, case let .payment(phoneNumber, _, _, syncContacts) = state.contents { + transaction.setState(UnauthorizedAccountState(isTestingEnvironment: testingEnvironment, masterDatacenterId: masterDatacenterId, contents: .confirmationCodeEntry(number: phoneNumber, type: SentAuthorizationCodeType(apiType: type), hash: phoneCodeHash, timeout: codeTimeout, nextType: parsedNextType, syncContacts: syncContacts, previousCodeEntry: nil, usePrevious: false))) + } + }).start() + case let .sentCodeSuccess(authorization): + switch authorization { + case let .authorization(_, _, _, futureAuthToken, user): + let _ = postbox.transaction({ [weak self] transaction in + var syncContacts = true + if let state = transaction.getState() as? UnauthorizedAccountState, case let .payment(_, _, _, syncContactsValue) = state.contents { + syncContacts = syncContactsValue + } + + if let futureAuthToken = futureAuthToken { + storeFutureLoginToken(accountManager: accountManager, token: futureAuthToken.makeData()) + } + + let user = TelegramUser(user: user) + var isSupportUser = false + if let phone = user.phone, phone.hasPrefix("42"), phone.count <= 5 { + isSupportUser = true + } + let state = AuthorizedAccountState(isTestingEnvironment: testingEnvironment, masterDatacenterId: masterDatacenterId, peerId: user.id, state: nil, invalidatedChannels: []) + initializedAppSettingsAfterLogin(transaction: transaction, appVersion: networkArguments.appVersion, syncContacts: syncContacts) + transaction.setState(state) + return accountManager.transaction { [weak self] transaction -> SendAuthorizationCodeResult in + if let self { + switchToAuthorizedAccount(transaction: transaction, account: self, isSupportUser: isSupportUser) + } + return .loggedIn + } + }).start() + case let .authorizationSignUpRequired(_, termsOfService): + let _ = postbox.transaction({ [weak self] transaction in + if let self { + if let state = transaction.getState() as? UnauthorizedAccountState, case let .payment(number, codeHash, _, syncContacts) = state.contents { + let _ = beginSignUp( + account: self, + data: AuthorizationSignUpData( + number: number, + codeHash: codeHash, + code: .phoneCode(""), + termsOfService: termsOfService.flatMap(UnauthorizedAccountTermsOfService.init(apiTermsOfService:)), + syncContacts: syncContacts + ) + ).start() + } + } + }).start() + } + } + } network.shouldKeepConnection.set(self.shouldBeServiceTaskMaster.get() |> map { mode -> Bool in @@ -152,7 +225,7 @@ public class UnauthorizedAccount { |> 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) + let updated = UnauthorizedAccount(accountManager: accountManager, 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()) return updated } @@ -250,7 +323,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)) + return .unauthorized(UnauthorizedAccount(accountManager: accountManager, networkArguments: networkArguments, id: id, rootPath: rootPath, basePath: path, testingEnvironment: unauthorizedState.isTestingEnvironment, postbox: postbox, network: network, shouldKeepAutoConnection: shouldKeepAutoConnection)) } case let authorizedState as AuthorizedAccountState: return postbox.transaction { transaction -> String? in @@ -269,7 +342,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)) + return .unauthorized(UnauthorizedAccount(accountManager: accountManager, networkArguments: networkArguments, id: id, rootPath: rootPath, basePath: path, testingEnvironment: beginWithTestingEnvironment, postbox: postbox, network: network, shouldKeepAutoConnection: shouldKeepAutoConnection)) } } } diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index 03276a38be..d3fe483a2e 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -227,7 +227,7 @@ func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] { } switch action { - case .messageActionChannelCreate, .messageActionChatDeletePhoto, .messageActionChatEditPhoto, .messageActionChatEditTitle, .messageActionEmpty, .messageActionPinMessage, .messageActionHistoryClear, .messageActionGameScore, .messageActionPaymentSent, .messageActionPaymentSentMe, .messageActionPhoneCall, .messageActionScreenshotTaken, .messageActionCustomAction, .messageActionBotAllowed, .messageActionSecureValuesSent, .messageActionSecureValuesSentMe, .messageActionContactSignUp, .messageActionGroupCall, .messageActionSetMessagesTTL, .messageActionGroupCallScheduled, .messageActionSetChatTheme, .messageActionChatJoinedByRequest, .messageActionWebViewDataSent, .messageActionWebViewDataSentMe, .messageActionGiftPremium, .messageActionGiftStars, .messageActionTopicCreate, .messageActionTopicEdit, .messageActionSuggestProfilePhoto, .messageActionSetChatWallPaper, .messageActionGiveawayLaunch, .messageActionGiveawayResults, .messageActionBoostApply, .messageActionRequestedPeerSentMe, .messageActionStarGift, .messageActionStarGiftUnique: + case .messageActionChannelCreate, .messageActionChatDeletePhoto, .messageActionChatEditPhoto, .messageActionChatEditTitle, .messageActionEmpty, .messageActionPinMessage, .messageActionHistoryClear, .messageActionGameScore, .messageActionPaymentSent, .messageActionPaymentSentMe, .messageActionPhoneCall, .messageActionScreenshotTaken, .messageActionCustomAction, .messageActionBotAllowed, .messageActionSecureValuesSent, .messageActionSecureValuesSentMe, .messageActionContactSignUp, .messageActionGroupCall, .messageActionSetMessagesTTL, .messageActionGroupCallScheduled, .messageActionSetChatTheme, .messageActionChatJoinedByRequest, .messageActionWebViewDataSent, .messageActionWebViewDataSentMe, .messageActionGiftPremium, .messageActionGiftStars, .messageActionTopicCreate, .messageActionTopicEdit, .messageActionSuggestProfilePhoto, .messageActionSetChatWallPaper, .messageActionGiveawayLaunch, .messageActionGiveawayResults, .messageActionBoostApply, .messageActionRequestedPeerSentMe, .messageActionStarGift, .messageActionStarGiftUnique, .messageActionPaidMessagesRefunded, .messageActionPaidMessagesPrice: break case let .messageActionChannelMigrateFrom(_, chatId): result.append(PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(chatId))) diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift index e4e7923481..2dccae65da 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift @@ -191,6 +191,10 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe return nil } return TelegramMediaAction(action: .starGiftUnique(gift: gift, isUpgrade: (flags & (1 << 0)) != 0, isTransferred: (flags & (1 << 1)) != 0, savedToProfile: (flags & (1 << 2)) != 0, canExportDate: canExportAt, transferStars: transferStars, isRefunded: (flags & (1 << 5)) != 0, peerId: peer?.peerId, senderId: fromId?.peerId, savedId: savedId)) + case let .messageActionPaidMessagesRefunded(count, stars): + return TelegramMediaAction(action: .paidMessagesRefunded(count: count, stars: stars)) + case let .messageActionPaidMessagesPrice(stars): + return TelegramMediaAction(action: .paidMessagesPriceEdited(stars: stars)) } } diff --git a/submodules/TelegramCore/Sources/Authorization.swift b/submodules/TelegramCore/Sources/Authorization.swift index bd12468878..836687ce7f 100644 --- a/submodules/TelegramCore/Sources/Authorization.swift +++ b/submodules/TelegramCore/Sources/Authorization.swift @@ -356,6 +356,8 @@ public func sendAuthorizationCode(accountManager: AccountManager UInt { - return 200 + return 201 } public func parseMessage(_ data: Data!) -> Any! { diff --git a/submodules/TelegramCore/Sources/State/UnauthorizedAccountStateManager.swift b/submodules/TelegramCore/Sources/State/UnauthorizedAccountStateManager.swift index d2b8582bdb..9a12e5608b 100644 --- a/submodules/TelegramCore/Sources/State/UnauthorizedAccountStateManager.swift +++ b/submodules/TelegramCore/Sources/State/UnauthorizedAccountStateManager.swift @@ -53,11 +53,18 @@ final class UnauthorizedAccountStateManager { private var updateService: UnauthorizedUpdateMessageService? private let updateServiceDisposable = MetaDisposable() private let updateLoginToken: () -> Void + private let updateSentCode: (Api.auth.SentCode) -> Void private let displayServiceNotification: (String) -> Void - init(network: Network, updateLoginToken: @escaping () -> Void, displayServiceNotification: @escaping (String) -> Void) { + init( + network: Network, + updateLoginToken: @escaping () -> Void, + updateSentCode: @escaping (Api.auth.SentCode) -> Void, + displayServiceNotification: @escaping (String) -> Void + ) { self.network = network self.updateLoginToken = updateLoginToken + self.updateSentCode = updateSentCode self.displayServiceNotification = displayServiceNotification } @@ -65,11 +72,18 @@ final class UnauthorizedAccountStateManager { self.updateServiceDisposable.dispose() } + func addUpdates(_ updates: Api.Updates) { + self.queue.async { + self.updateService?.addUpdates(updates) + } + } + func reset() { self.queue.async { if self.updateService == nil { self.updateService = UnauthorizedUpdateMessageService() let updateLoginToken = self.updateLoginToken + let updateSentCode = self.updateSentCode let displayServiceNotification = self.displayServiceNotification self.updateServiceDisposable.set(self.updateService!.pipe.signal().start(next: { updates in for update in updates { @@ -81,6 +95,8 @@ final class UnauthorizedAccountStateManager { if popup { displayServiceNotification(message) } + case let .updateSentPhoneCode(sentCode): + updateSentCode(sentCode) default: break } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift index 6dc1fdecb2..513375d750 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift @@ -132,6 +132,8 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case prizeStars(amount: Int64, isUnclaimed: Bool, boostPeerId: PeerId?, transactionId: String?, giveawayMessageId: MessageId?) case starGift(gift: StarGift, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool, upgraded: Bool, canUpgrade: Bool, upgradeStars: Int64?, isRefunded: Bool, upgradeMessageId: Int32?, peerId: EnginePeer.Id?, senderId: EnginePeer.Id?, savedId: Int64?) case starGiftUnique(gift: StarGift, isUpgrade: Bool, isTransferred: Bool, savedToProfile: Bool, canExportDate: Int32?, transferStars: Int64?, isRefunded: Bool, peerId: EnginePeer.Id?, senderId: EnginePeer.Id?, savedId: Int64?) + case paidMessagesRefunded(count: Int32, stars: Int64) + case paidMessagesPriceEdited(stars: Int64) public init(decoder: PostboxDecoder) { let rawValue: Int32 = decoder.decodeInt32ForKey("_rawValue", orElse: 0) @@ -256,6 +258,10 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { self = .starGift(gift: decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift, convertStars: decoder.decodeOptionalInt64ForKey("convertStars"), text: decoder.decodeOptionalStringForKey("text"), entities: decoder.decodeOptionalObjectArrayWithDecoderForKey("entities"), nameHidden: decoder.decodeBoolForKey("nameHidden", orElse: false), savedToProfile: decoder.decodeBoolForKey("savedToProfile", orElse: false), converted: decoder.decodeBoolForKey("converted", orElse: false), upgraded: decoder.decodeBoolForKey("upgraded", orElse: false), canUpgrade: decoder.decodeBoolForKey("canUpgrade", orElse: false), upgradeStars: decoder.decodeOptionalInt64ForKey("upgradeStars"), isRefunded: decoder.decodeBoolForKey("isRefunded", orElse: false), upgradeMessageId: decoder.decodeOptionalInt32ForKey("upgradeMessageId"), peerId: decoder.decodeOptionalInt64ForKey("peerId").flatMap { EnginePeer.Id($0) }, senderId: decoder.decodeOptionalInt64ForKey("senderId").flatMap { EnginePeer.Id($0) }, savedId: decoder.decodeOptionalInt64ForKey("savedId")) case 45: self = .starGiftUnique(gift: decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift, isUpgrade: decoder.decodeBoolForKey("isUpgrade", orElse: false), isTransferred: decoder.decodeBoolForKey("isTransferred", orElse: false), savedToProfile: decoder.decodeBoolForKey("savedToProfile", orElse: false), canExportDate: decoder.decodeOptionalInt32ForKey("canExportDate"), transferStars: decoder.decodeOptionalInt64ForKey("transferStars"), isRefunded: decoder.decodeBoolForKey("isRefunded", orElse: false), peerId: decoder.decodeOptionalInt64ForKey("peerId").flatMap { EnginePeer.Id($0) }, senderId: decoder.decodeOptionalInt64ForKey("senderId").flatMap { EnginePeer.Id($0) }, savedId: decoder.decodeOptionalInt64ForKey("savedId")) + case 46: + self = .paidMessagesRefunded(count: decoder.decodeInt32ForKey("count", orElse: 0), stars: decoder.decodeInt64ForKey("stars", orElse: 0)) + case 47: + self = .paidMessagesPriceEdited(stars: decoder.decodeInt64ForKey("stars", orElse: 0)) default: self = .unknown } @@ -626,6 +632,13 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { } else { encoder.encodeNil(forKey: "savedId") } + case let .paidMessagesRefunded(count, stars): + encoder.encodeInt32(46, forKey: "_rawValue") + encoder.encodeInt32(count, forKey: "count") + encoder.encodeInt64(stars, forKey: "stars") + case let .paidMessagesPriceEdited(stars): + encoder.encodeInt32(47, forKey: "_rawValue") + encoder.encodeInt64(stars, forKey: "stars") } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_UnauthorizedAccountState.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_UnauthorizedAccountState.swift index 9bab8302f8..20b3ca184b 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_UnauthorizedAccountState.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_UnauthorizedAccountState.swift @@ -139,6 +139,7 @@ private enum UnauthorizedAccountStateContentsValue: Int32 { case signUp = 5 case passwordRecovery = 6 case awaitingAccountReset = 7 + case payment = 8 } public struct UnauthorizedAccountTermsOfService: PostboxCoding, Equatable { @@ -181,6 +182,7 @@ public indirect enum UnauthorizedAccountStateContents: PostboxCoding, Equatable case passwordRecovery(hint: String, number: String?, code: AuthorizationCode?, emailPattern: String, syncContacts: Bool) case awaitingAccountReset(protectedUntil: Int32, number: String?, syncContacts: Bool) case signUp(number: String, codeHash: String, firstName: String, lastName: String, termsOfService: UnauthorizedAccountTermsOfService?, syncContacts: Bool) + case payment(number: String, codeHash: String, storeProduct: String, syncContacts: Bool) public init(decoder: PostboxDecoder) { switch decoder.decodeInt32ForKey("v", orElse: 0) { @@ -214,6 +216,8 @@ public indirect enum UnauthorizedAccountStateContents: PostboxCoding, Equatable self = .awaitingAccountReset(protectedUntil: decoder.decodeInt32ForKey("protectedUntil", orElse: 0), number: decoder.decodeOptionalStringForKey("number"), syncContacts: decoder.decodeInt32ForKey("syncContacts", orElse: 1) != 0) case UnauthorizedAccountStateContentsValue.signUp.rawValue: self = .signUp(number: decoder.decodeStringForKey("n", orElse: ""), codeHash: decoder.decodeStringForKey("h", orElse: ""), firstName: decoder.decodeStringForKey("f", orElse: ""), lastName: decoder.decodeStringForKey("l", orElse: ""), termsOfService: decoder.decodeObjectForKey("tos", decoder: { UnauthorizedAccountTermsOfService(decoder: $0) }) as? UnauthorizedAccountTermsOfService, syncContacts: decoder.decodeInt32ForKey("syncContacts", orElse: 1) != 0) + case UnauthorizedAccountStateContentsValue.payment.rawValue: + self = .payment(number: decoder.decodeStringForKey("n", orElse: ""), codeHash: decoder.decodeStringForKey("h", orElse: ""), storeProduct: decoder.decodeStringForKey("storeProduct", orElse: ""), syncContacts: decoder.decodeInt32ForKey("syncContacts", orElse: 1) != 0) default: assertionFailure() self = .empty @@ -303,6 +307,12 @@ public indirect enum UnauthorizedAccountStateContents: PostboxCoding, Equatable encoder.encodeNil(forKey: "tos") } encoder.encodeInt32(syncContacts ? 1 : 0, forKey: "syncContacts") + case let .payment(number, codeHash, storeProduct, syncContacts): + encoder.encodeInt32(UnauthorizedAccountStateContentsValue.payment.rawValue, forKey: "v") + encoder.encodeString(number, forKey: "n") + encoder.encodeString(codeHash, forKey: "h") + encoder.encodeString(storeProduct, forKey: "storeProduct") + encoder.encodeInt32(syncContacts ? 1 : 0, forKey: "syncContacts") } } @@ -374,6 +384,12 @@ public indirect enum UnauthorizedAccountStateContents: PostboxCoding, Equatable } else { return false } + case let .payment(number, codeHash, storeProduct, syncContacts): + if case .payment(number, codeHash, storeProduct, syncContacts) = rhs { + return true + } else { + return false + } } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/AccountData/ChangeAccountPhoneNumber.swift b/submodules/TelegramCore/Sources/TelegramEngine/AccountData/ChangeAccountPhoneNumber.swift index 7666b030d4..537d05ee5e 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/AccountData/ChangeAccountPhoneNumber.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/AccountData/ChangeAccountPhoneNumber.swift @@ -113,7 +113,7 @@ func _internal_requestChangeAccountPhoneNumberVerification(account: Account, api } else { return .single(ChangeAccountPhoneNumberData(type: SentAuthorizationCodeType(apiType: type), hash: phoneCodeHash, timeout: codeTimeout, nextType: parsedNextType)) } - case .sentCodeSuccess: + case .sentCodeSuccess, .sentCodePaymentRequired: return .never() } } @@ -188,7 +188,7 @@ private func internalResendChangeAccountPhoneNumberVerification(account: Account } else { return .single(ChangeAccountPhoneNumberData(type: SentAuthorizationCodeType(apiType: type), hash: phoneCodeHash, timeout: codeTimeout, nextType: parsedNextType)) } - case .sentCodeSuccess: + case .sentCodeSuccess, .sentCodePaymentRequired: return .never() } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Auth/CancelAccountReset.swift b/submodules/TelegramCore/Sources/TelegramEngine/Auth/CancelAccountReset.swift index b0bd632e63..b1dc5b9705 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Auth/CancelAccountReset.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Auth/CancelAccountReset.swift @@ -34,7 +34,7 @@ func _internal_requestCancelAccountResetData(network: Network, hash: String) -> parsedNextType = AuthorizationCodeNextType(apiType: nextType) } return .single(CancelAccountResetData(type: SentAuthorizationCodeType(apiType: type), hash: phoneCodeHash, timeout: timeout, nextType: parsedNextType)) - case .sentCodeSuccess: + case .sentCodeSuccess, .sentCodePaymentRequired: return .never() } } @@ -57,7 +57,7 @@ func _internal_requestNextCancelAccountResetOption(network: Network, phoneNumber parsedNextType = AuthorizationCodeNextType(apiType: nextType) } return .single(CancelAccountResetData(type: SentAuthorizationCodeType(apiType: type), hash: phoneCodeHash, timeout: timeout, nextType: parsedNextType)) - case .sentCodeSuccess: + case .sentCodeSuccess, .sentCodePaymentRequired: return .never() } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/ItemCache/TelegramEngineItemCache.swift b/submodules/TelegramCore/Sources/TelegramEngine/ItemCache/TelegramEngineItemCache.swift index 8723e0ec22..a3ed8f1092 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/ItemCache/TelegramEngineItemCache.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/ItemCache/TelegramEngineItemCache.swift @@ -38,3 +38,44 @@ public extension TelegramEngine { } } } + +public extension TelegramEngineUnauthorized { + final class ItemCache { + private let account: UnauthorizedAccount + + init(account: UnauthorizedAccount) { + self.account = account + } + + public func put(collectionId: Int8, id: EngineDataBuffer, item: T) -> Signal { + return self.account.postbox.transaction { transaction -> Void in + if let entry = CodableEntry(item) { + transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: collectionId, key: id), entry: entry) + } + } + |> ignoreValues + } + + public func get(collectionId: Int8, id: EngineDataBuffer) -> Signal { + return self.account.postbox.transaction { transaction -> CodableEntry? in + return transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: collectionId, key: id)) + } + } + + public func remove(collectionId: Int8, id: EngineDataBuffer) -> Signal { + return self.account.postbox.transaction { transaction -> Void in + transaction.removeItemCacheEntry(id: ItemCacheEntryId(collectionId: collectionId, key: id)) + } + |> ignoreValues + } + + public func clear(collectionIds: [Int8]) -> Signal { + return self.account.postbox.transaction { transaction -> Void in + for id in collectionIds { + transaction.clearItemCacheCollection(collectionId: id) + } + } + |> ignoreValues + } + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift index f282a1ad39..4c7c5b1471 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift @@ -1072,7 +1072,7 @@ public func _internal_setAccountConnectedBot(account: Account, bot: TelegramAcco flags |= 1 << 1 } - return account.network.request(Api.functions.account.updateConnectedBot(flags: flags, bot: mappedBot, recipients: mappedRecipients)) + return account.network.request(Api.functions.account.updateConnectedBot(flags: flags, rights: nil, bot: mappedBot, recipients: mappedRecipients)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift index b9630127c2..01422837b6 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift @@ -109,7 +109,14 @@ func _internal_peerSendAsAvailablePeers(accountPeerId: PeerId, network: Network, return .single([]) } - if let channel = peer as? TelegramChannel, case .group = channel.info { + if let channel = peer as? TelegramChannel { + if case .group = channel.info { + + } else if channel.adminRights != nil || channel.flags.contains(.isCreator) { + + } else { + return .single([]) + } } else { return .single([]) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift index b30434af1b..c89af92e93 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift @@ -20,9 +20,10 @@ public enum AppStoreTransactionPurpose { case stars(count: Int64, currency: String, amount: Int64) case starsGift(peerId: EnginePeer.Id, count: Int64, currency: String, amount: Int64) case starsGiveaway(stars: Int64, boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, showWinners: Bool, prizeDescription: String?, randomId: Int64, untilDate: Int32, currency: String, amount: Int64, users: Int32) + case authCode(restore: Bool, phoneNumber: String, phoneCodeHash: String, currency: String, amount: Int64) } -private func apiInputStorePaymentPurpose(account: Account, purpose: AppStoreTransactionPurpose) -> Signal { +private func apiInputStorePaymentPurpose(postbox: Postbox, purpose: AppStoreTransactionPurpose) -> Signal { switch purpose { case .subscription, .upgrade, .restore: var flags: Int32 = 0 @@ -36,7 +37,7 @@ private func apiInputStorePaymentPurpose(account: Account, purpose: AppStoreTran } return .single(.inputStorePaymentPremiumSubscription(flags: flags)) case let .gift(peerId, currency, amount): - return account.postbox.loadedPeerWithId(peerId) + return postbox.loadedPeerWithId(peerId) |> mapToSignal { peer -> Signal in guard let inputUser = apiInputUser(peer) else { return .complete() @@ -44,7 +45,7 @@ private func apiInputStorePaymentPurpose(account: Account, purpose: AppStoreTran return .single(.inputStorePaymentGiftPremium(userId: inputUser, currency: currency, amount: amount)) } case let .giftCode(peerIds, boostPeerId, currency, amount, text, entities): - return account.postbox.transaction { transaction -> Api.InputStorePaymentPurpose in + return postbox.transaction { transaction -> Api.InputStorePaymentPurpose in var flags: Int32 = 0 var apiBoostPeer: Api.InputPeer? var apiInputUsers: [Api.InputUser] = [] @@ -69,7 +70,7 @@ private func apiInputStorePaymentPurpose(account: Account, purpose: AppStoreTran return .inputStorePaymentPremiumGiftCode(flags: flags, users: apiInputUsers, boostPeer: apiBoostPeer, currency: currency, amount: amount, message: message) } case let .giveaway(boostPeerId, additionalPeerIds, countries, onlyNewSubscribers, showWinners, prizeDescription, randomId, untilDate, currency, amount): - return account.postbox.transaction { transaction -> Signal in + return postbox.transaction { transaction -> Signal in guard let peer = transaction.getPeer(boostPeerId), let apiBoostPeer = apiInputPeer(peer) else { return .complete() } @@ -101,7 +102,7 @@ private func apiInputStorePaymentPurpose(account: Account, purpose: AppStoreTran case let .stars(count, currency, amount): return .single(.inputStorePaymentStarsTopup(stars: count, currency: currency, amount: amount)) case let .starsGift(peerId, count, currency, amount): - return account.postbox.loadedPeerWithId(peerId) + return postbox.loadedPeerWithId(peerId) |> mapToSignal { peer -> Signal in guard let inputUser = apiInputUser(peer) else { return .complete() @@ -109,7 +110,7 @@ private func apiInputStorePaymentPurpose(account: Account, purpose: AppStoreTran return .single(.inputStorePaymentStarsGift(userId: inputUser, stars: count, currency: currency, amount: amount)) } case let .starsGiveaway(stars, boostPeerId, additionalPeerIds, countries, onlyNewSubscribers, showWinners, prizeDescription, randomId, untilDate, currency, amount, users): - return account.postbox.transaction { transaction -> Signal in + return postbox.transaction { transaction -> Signal in guard let peer = transaction.getPeer(boostPeerId), let apiBoostPeer = apiInputPeer(peer) else { return .complete() } @@ -138,14 +139,20 @@ private func apiInputStorePaymentPurpose(account: Account, purpose: AppStoreTran return .single(.inputStorePaymentStarsGiveaway(flags: flags, stars: stars, boostPeer: apiBoostPeer, additionalPeers: additionalPeers, countriesIso2: countries, prizeDescription: prizeDescription, randomId: randomId, untilDate: untilDate, currency: currency, amount: amount, users: users)) } |> switchToLatest + case let .authCode(restore, phoneNumber, phoneCodeHash, currency, amount): + var flags: Int32 = 0 + if restore { + flags |= (1 << 0) + } + return .single(.inputStorePaymentAuthCode(flags: flags, phoneNumber: phoneNumber, phoneCodeHash: phoneCodeHash, currency: currency, amount: amount)) } } -func _internal_sendAppStoreReceipt(account: Account, receipt: Data, purpose: AppStoreTransactionPurpose) -> Signal { - return apiInputStorePaymentPurpose(account: account, purpose: purpose) +func _internal_sendAppStoreReceipt(postbox: Postbox, network: Network, stateManager: AccountStateManager, receipt: Data, purpose: AppStoreTransactionPurpose) -> Signal { + return apiInputStorePaymentPurpose(postbox: postbox, purpose: purpose) |> castError(AssignAppStoreTransactionError.self) |> mapToSignal { purpose -> Signal in - return account.network.request(Api.functions.payments.assignAppStoreTransaction(receipt: Buffer(data: receipt), purpose: purpose)) + return network.request(Api.functions.payments.assignAppStoreTransaction(receipt: Buffer(data: receipt), purpose: purpose)) |> mapError { error -> AssignAppStoreTransactionError in if error.errorCode == 406 { return .serverProvided @@ -154,7 +161,26 @@ func _internal_sendAppStoreReceipt(account: Account, receipt: Data, purpose: App } } |> mapToSignal { updates -> Signal in - account.stateManager.addUpdates(updates) + stateManager.addUpdates(updates) + return .complete() + } + } +} + +func _internal_sendAppStoreReceipt(postbox: Postbox, network: Network, stateManager: UnauthorizedAccountStateManager, receipt: Data, purpose: AppStoreTransactionPurpose) -> Signal { + return apiInputStorePaymentPurpose(postbox: postbox, purpose: purpose) + |> castError(AssignAppStoreTransactionError.self) + |> mapToSignal { purpose -> Signal in + return network.request(Api.functions.payments.assignAppStoreTransaction(receipt: Buffer(data: receipt), purpose: purpose)) + |> mapError { error -> AssignAppStoreTransactionError in + if error.errorCode == 406 { + return .serverProvided + } else { + return .generic + } + } + |> mapToSignal { updates -> Signal in + stateManager.addUpdates(updates) return .complete() } } @@ -164,10 +190,10 @@ public enum RestoreAppStoreReceiptError { case generic } -func _internal_canPurchasePremium(account: Account, purpose: AppStoreTransactionPurpose) -> Signal { - return apiInputStorePaymentPurpose(account: account, purpose: purpose) +func _internal_canPurchasePremium(postbox: Postbox, network: Network, purpose: AppStoreTransactionPurpose) -> Signal { + return apiInputStorePaymentPurpose(postbox: postbox, purpose: purpose) |> mapToSignal { purpose -> Signal in - return account.network.request(Api.functions.payments.canPurchasePremium(purpose: purpose)) + return network.request(Api.functions.payments.canPurchaseStore(purpose: purpose)) |> map { result -> Bool in switch result { case .boolTrue: diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift index b5c2e11a66..8817f48613 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift @@ -395,7 +395,7 @@ func _internal_parseInputInvoice(transaction: Transaction, source: BotPaymentInv var flags: Int32 = 0 var message: Api.TextWithEntities? if let text, !text.isEmpty { - flags |= (1 << 1) + flags |= (1 << 0) message = .textWithEntities(text: text, entities: entities.flatMap { apiEntitiesFromMessageTextEntities($0, associatedPeers: SimpleDictionary()) } ?? []) } return .inputInvoicePremiumGiftStars(flags: flags, userId: inputUser, months: option.months, message: message) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift index 2587b5fc2b..13c30a2bb6 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift @@ -568,6 +568,21 @@ private final class StarsContextImpl { self._state = state self._statePromise.set(.single(state)) } + + var onUpdate: Signal { + return self._statePromise.get() + |> take(until: { value in + if let value { + if !value.flags.contains(.isPendingBalance) { + return SignalTakeAction(passthrough: true, complete: true) + } + } + return SignalTakeAction(passthrough: false, complete: false) + }) + |> map { _ in + return Void() + } + } } private extension StarsContext.State.Transaction { @@ -1011,6 +1026,17 @@ public final class StarsContext { } } + public var onUpdate: Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + self.impl.with { impl in + disposable.set(impl.onUpdate.start(next: { value in + subscriber.putNext(value) + })) + } + return disposable + } + } init(account: Account) { self.impl = QueueLocalObject(queue: Queue.mainQueue(), generate: { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift index 0f6f152b40..7c1a697456 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift @@ -39,11 +39,11 @@ public extension TelegramEngine { } public func sendAppStoreReceipt(receipt: Data, purpose: AppStoreTransactionPurpose) -> Signal { - return _internal_sendAppStoreReceipt(account: self.account, receipt: receipt, purpose: purpose) + return _internal_sendAppStoreReceipt(postbox: self.account.postbox, network: self.account.network, stateManager: self.account.stateManager, receipt: receipt, purpose: purpose) } public func canPurchasePremium(purpose: AppStoreTransactionPurpose) -> Signal { - return _internal_canPurchasePremium(account: self.account, purpose: purpose) + return _internal_canPurchasePremium(postbox: self.account.postbox, network: self.account.network, purpose: purpose) } public func checkPremiumGiftCode(slug: String) -> Signal { @@ -150,3 +150,21 @@ public extension TelegramEngine { } } } + +public extension TelegramEngineUnauthorized { + final class Payments { + private let account: UnauthorizedAccount + + init(account: UnauthorizedAccount) { + self.account = account + } + + public func canPurchasePremium(purpose: AppStoreTransactionPurpose) -> Signal { + return _internal_canPurchasePremium(postbox: self.account.postbox, network: self.account.network, purpose: purpose) + } + + public func sendAppStoreReceipt(receipt: Data, purpose: AppStoreTransactionPurpose) -> Signal { + return _internal_sendAppStoreReceipt(postbox: self.account.postbox, network: self.account.network, stateManager: self.account.stateManager, receipt: receipt, purpose: purpose) + } + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift index 9c68e06e69..4212a15e8e 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift @@ -246,7 +246,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee if let apiBot = connectedBots.first { switch apiBot { - case let .connectedBot(flags, botId, recipients): + case let .connectedBot(flags, botId, recipients, _): mappedConnectedBot = TelegramAccountConnectedBot( id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(botId)), recipients: TelegramBusinessRecipients(apiValue: recipients), diff --git a/submodules/TelegramCore/Sources/TelegramEngine/SecureId/VerifySecureIdValue.swift b/submodules/TelegramCore/Sources/TelegramEngine/SecureId/VerifySecureIdValue.swift index 7f0428cc04..400feb5289 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/SecureId/VerifySecureIdValue.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/SecureId/VerifySecureIdValue.swift @@ -30,7 +30,7 @@ public func secureIdPreparePhoneVerification(network: Network, value: SecureIdPh switch sentCode { case let .sentCode(_, type, phoneCodeHash, nextType, timeout): return .single(SecureIdPreparePhoneVerificationPayload(type: SentAuthorizationCodeType(apiType: type), nextType: nextType.flatMap(AuthorizationCodeNextType.init), timeout: timeout, phone: value.phone, phoneCodeHash: phoneCodeHash)) - case .sentCodeSuccess: + case .sentCodeSuccess, .sentCodePaymentRequired: return .never() } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/TelegramEngine.swift b/submodules/TelegramCore/Sources/TelegramEngine/TelegramEngine.swift index 05a80077d9..35a49e9ea3 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/TelegramEngine.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/TelegramEngine.swift @@ -107,6 +107,14 @@ public final class TelegramEngineUnauthorized { public lazy var localization: Localization = { return Localization(account: self.account) }() + + public lazy var payments: Payments = { + return Payments(account: self.account) + }() + + public lazy var itemCache: ItemCache = { + return ItemCache(account: self.account) + }() } public enum SomeTelegramEngine { diff --git a/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift b/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift index 5143dcad64..a2c2a1dabb 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift @@ -170,6 +170,12 @@ public final class PrincipalThemeEssentialGraphics { public let outgoingDateAndStatusRepliesIcon: UIImage public let mediaRepliesIcon: UIImage public let freeRepliesIcon: UIImage + + public let incomingDateAndStatusStarsIcon: UIImage + public let outgoingDateAndStatusStarsIcon: UIImage + public let mediaStarsIcon: UIImage + public let freeStarsIcon: UIImage + public let incomingDateAndStatusPinnedIcon: UIImage public let outgoingDateAndStatusPinnedIcon: UIImage public let mediaPinnedIcon: UIImage @@ -358,6 +364,12 @@ public final class PrincipalThemeEssentialGraphics { self.mediaRepliesIcon = generateTintedImage(image: repliesImage, color: .white)! self.freeRepliesIcon = generateTintedImage(image: repliesImage, color: serviceColor.primaryText)! + let starsImage = UIImage(bundleImageName: "Chat/Message/StarsCount")! + self.incomingDateAndStatusStarsIcon = generateTintedImage(image: starsImage, color: theme.message.incoming.secondaryTextColor)! + self.outgoingDateAndStatusStarsIcon = generateTintedImage(image: starsImage, color: theme.message.outgoing.secondaryTextColor)! + self.mediaStarsIcon = generateTintedImage(image: starsImage, color: .white)! + self.freeStarsIcon = generateTintedImage(image: starsImage, color: serviceColor.primaryText)! + let pinnedImage = UIImage(bundleImageName: "Chat/Message/Pinned")! self.incomingDateAndStatusPinnedIcon = generateTintedImage(image: pinnedImage, color: theme.message.incoming.secondaryTextColor)! self.outgoingDateAndStatusPinnedIcon = generateTintedImage(image: pinnedImage, color: theme.message.outgoing.secondaryTextColor)! @@ -479,6 +491,12 @@ public final class PrincipalThemeEssentialGraphics { self.mediaRepliesIcon = generateTintedImage(image: repliesImage, color: .white)! self.freeRepliesIcon = generateTintedImage(image: repliesImage, color: serviceColor.primaryText)! + let starsImage = UIImage(bundleImageName: "Chat/Message/StarsCount")! + self.incomingDateAndStatusStarsIcon = generateTintedImage(image: starsImage, color: theme.message.incoming.secondaryTextColor)! + self.outgoingDateAndStatusStarsIcon = generateTintedImage(image: starsImage, color: theme.message.outgoing.secondaryTextColor)! + self.mediaStarsIcon = generateTintedImage(image: starsImage, color: .white)! + self.freeStarsIcon = generateTintedImage(image: starsImage, color: serviceColor.primaryText)! + let pinnedImage = UIImage(bundleImageName: "Chat/Message/Pinned")! self.incomingDateAndStatusPinnedIcon = generateTintedImage(image: pinnedImage, color: theme.message.incoming.secondaryTextColor)! self.outgoingDateAndStatusPinnedIcon = generateTintedImage(image: pinnedImage, color: theme.message.outgoing.secondaryTextColor)! diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index 89a13d50e5..4f75317468 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -1196,6 +1196,33 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, } } } + case let .paidMessagesRefunded(_, stars): + let starsString = strings.Notification_PaidMessageRefund_Stars(Int32(stars)) + if message.author?.id == accountPeerId, let messagePeer = message.peers[message.id.peerId] { + let peerName = EnginePeer(messagePeer).compactDisplayTitle + var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(1, messagePeer.id)]) + attributes[0] = boldAttributes + let resultString = strings.Notification_PaidMessageRefundYou(starsString, peerName) + attributedString = addAttributesToStringWithRanges(resultString._tuple, body: bodyAttributes, argumentAttributes: attributes) + } else { + let peerName = message.author?.compactDisplayTitle ?? "" + var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id)]) + attributes[1] = boldAttributes + let resultString = strings.Notification_PaidMessageRefund(peerName, starsString) + attributedString = addAttributesToStringWithRanges(resultString._tuple, body: bodyAttributes, argumentAttributes: attributes) + } + case let .paidMessagesPriceEdited(stars): + let starsString = strings.Notification_PaidMessagePriceChanged_Stars(Int32(stars)) + if message.author?.id == accountPeerId { + let resultString = strings.Notification_PaidMessagePriceChangedYou(starsString) + attributedString = addAttributesToStringWithRanges(resultString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes]) + } else { + let peerName = message.author?.compactDisplayTitle ?? "" + var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id)]) + attributes[1] = boldAttributes + let resultString = strings.Notification_PaidMessagePriceChanged(peerName, starsString) + attributedString = addAttributesToStringWithRanges(resultString._tuple, body: bodyAttributes, argumentAttributes: attributes) + } case .unknown: attributedString = nil } diff --git a/submodules/TelegramStringFormatting/Sources/TonFormat.swift b/submodules/TelegramStringFormatting/Sources/TonFormat.swift index de64f93e82..c237580f34 100644 --- a/submodules/TelegramStringFormatting/Sources/TonFormat.swift +++ b/submodules/TelegramStringFormatting/Sources/TonFormat.swift @@ -81,7 +81,7 @@ public func formatTonAmountText(_ value: Int64, dateTimeFormat: PresentationDate public func formatStarsAmountText(_ amount: StarsAmount, dateTimeFormat: PresentationDateTimeFormat, showPlus: Bool = false) -> String { var balanceText = presentationStringsFormattedNumber(Int32(amount.value), dateTimeFormat.groupingSeparator) - let fraction = Double(amount.nanos) / 10e6 + let fraction = abs(Double(amount.nanos)) / 10e6 if fraction > 0.0 { balanceText.append(dateTimeFormat.decimalSeparator) balanceText.append("\(Int32(fraction))") diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index ce5fdeaf86..4238867181 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -466,6 +466,7 @@ swift_library( "//submodules/TelegramUI/Components/ContentReportScreen", "//submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen", "//submodules/TelegramUI/Components/Stars/StarsBalanceOverlayComponent", + "//submodules/TelegramUI/Components/Settings/AccountFreezeInfoScreen", "//third-party/recaptcha:RecaptchaEnterprise", ] + select({ "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, diff --git a/submodules/TelegramUI/Components/Ads/AdsInfoScreen/Sources/AdsInfoScreen.swift b/submodules/TelegramUI/Components/Ads/AdsInfoScreen/Sources/AdsInfoScreen.swift index 7ad32eb899..65f23993ee 100644 --- a/submodules/TelegramUI/Components/Ads/AdsInfoScreen/Sources/AdsInfoScreen.swift +++ b/submodules/TelegramUI/Components/Ads/AdsInfoScreen/Sources/AdsInfoScreen.swift @@ -193,6 +193,7 @@ private final class ScrollContent: CombinedComponent { adsText = strings.AdsInfo_Bot_Ads_Text infoRawText = strings.AdsInfo_Bot_Launch_Text case .search: + //TODO:localize respectText = "Ads like this do not use your personal information and are based on the search query you entered." adsText = strings.AdsInfo_Bot_Ads_Text infoRawText = "Anyone can create an ad to display in search results for any query. Check out the Telegram Ad Platform for details. [Learn More >]()" diff --git a/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift b/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift index b63ae30563..0a6f696f34 100644 --- a/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift @@ -1213,9 +1213,9 @@ public final class ChatEmptyNodePremiumRequiredChatContent: ASDisplayNode, ChatE private var currentTheme: PresentationTheme? private var currentStrings: PresentationStrings? - private let stars: StarsAmount? + private let stars: Int64? - public init(context: AccountContext, interaction: ChatPanelInterfaceInteraction?, stars: StarsAmount?) { + public init(context: AccountContext, interaction: ChatPanelInterfaceInteraction?, stars: Int64?) { let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) self.isPremiumDisabled = premiumConfiguration.isPremiumDisabled self.stars = stars @@ -1295,7 +1295,7 @@ public final class ChatEmptyNodePremiumRequiredChatContent: ASDisplayNode, ChatE } ) if let amount = self.stars { - let starsString = presentationStringsFormattedNumber(Int32(amount.value), interfaceState.dateTimeFormat.groupingSeparator) + let starsString = presentationStringsFormattedNumber(Int32(amount), interfaceState.dateTimeFormat.groupingSeparator) let rawText: String if self.isPremiumDisabled { rawText = interfaceState.strings.Chat_EmptyStatePaidMessagingDisabled_Text(peerTitle, " $ \(starsString)").string @@ -1426,7 +1426,7 @@ private enum ChatEmptyNodeContentType: Equatable { case greeting case topic case premiumRequired - case starsRequired + case starsRequired(Int64) } private final class EmptyAttachedDescriptionNode: HighlightTrackingButtonNode { @@ -1787,7 +1787,7 @@ public final class ChatEmptyNode: ASDisplayNode { isScheduledMessages = true } - let contentType: ChatEmptyNodeContentType + var contentType: ChatEmptyNodeContentType var displayAttachedDescription = false switch subject { case .detailsPlaceholder: @@ -1815,8 +1815,8 @@ public final class ChatEmptyNode: ASDisplayNode { } else if let _ = interfaceState.peerNearbyData { contentType = .peerNearby } else if let peer = peer as? TelegramUser { - if let _ = interfaceState.sendPaidMessageStars { - contentType = .starsRequired + if let sendPaidMessageStars = interfaceState.sendPaidMessageStars, interfaceState.businessIntro == nil { + contentType = .starsRequired(sendPaidMessageStars.value) } else if interfaceState.isPremiumRequiredForMessaging { contentType = .premiumRequired } else { @@ -1881,8 +1881,8 @@ public final class ChatEmptyNode: ASDisplayNode { node = ChatEmptyNodeTopicChatContent(context: self.context) case .premiumRequired: node = ChatEmptyNodePremiumRequiredChatContent(context: self.context, interaction: self.interaction, stars: nil) - case .starsRequired: - node = ChatEmptyNodePremiumRequiredChatContent(context: self.context, interaction: self.interaction, stars: interfaceState.sendPaidMessageStars) + case let .starsRequired(stars): + node = ChatEmptyNodePremiumRequiredChatContent(context: self.context, interaction: self.interaction, stars: stars) } self.content = (contentType, node) self.addSubnode(node) @@ -1893,8 +1893,13 @@ public final class ChatEmptyNode: ASDisplayNode { node.layer.animateScale(from: 0.0, to: 1.0, duration: duration, timingFunction: curve.timingFunction) } } - self.isUserInteractionEnabled = [.peerNearby, .greeting, .premiumRequired, .starsRequired, .cloud].contains(contentType) - + switch contentType { + case .peerNearby, .greeting, .premiumRequired, .starsRequired, .cloud: + self.isUserInteractionEnabled = true + default: + self.isUserInteractionEnabled = false + } + let displayRect = CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: size.width, height: size.height - insets.top - insets.bottom)) var contentSize = CGSize() diff --git a/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift b/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift index e45c8961d1..36ef1d2e41 100644 --- a/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift +++ b/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift @@ -687,6 +687,8 @@ public final class ChatInlineSearchResultsListComponent: Component { openPhotoSetup: { }, openAdInfo: { _ in + }, + openAccountFreezeInfo: { } ) self.chatListNodeInteraction = chatListNodeInteraction diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift index 1064b9d35a..d967016c40 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -1044,6 +1044,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { var edited = false var viewCount: Int? = nil var dateReplies = 0 + var starsCount: Int64? var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeerId: item.context.account.peerId, accountPeer: item.associatedData.accountPeer, message: item.message) if item.message.isRestricted(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) { dateReactionsAndPeers = ([], []) @@ -1057,6 +1058,8 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .group = channel.info { dateReplies = Int(attribute.count) } + } else if let attribute = attribute as? PaidStarsMessageAttribute, item.message.id.peerId.namespace == Namespaces.Peer.CloudChannel { + starsCount = attribute.stars.value } } @@ -1086,6 +1089,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { areReactionsTags: item.message.areReactionsTags(accountPeerId: item.context.account.peerId), messageEffect: messageEffect, replyCount: dateReplies, + starsCount: starsCount, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, hasAutoremove: item.message.isSelfExpiring, canViewReactionList: canViewMessageReactionList(message: item.message), diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift index 297d1a9980..2df108a4e8 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift @@ -675,6 +675,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { } var viewCount: Int? var dateReplies = 0 + var starsCount: Int64? var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeerId: context.account.peerId, accountPeer: associatedData.accountPeer, message: message) if message.isRestricted(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) || presentationData.isPreview { dateReactionsAndPeers = ([], []) @@ -688,6 +689,8 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .group = channel.info { dateReplies = Int(attribute.count) } + } else if let attribute = attribute as? PaidStarsMessageAttribute, message.id.peerId.namespace == Namespaces.Peer.CloudChannel { + starsCount = attribute.stars.value } } @@ -747,6 +750,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { areReactionsTags: message.areReactionsTags(accountPeerId: context.account.peerId), messageEffect: message.messageEffect(availableMessageEffects: associatedData.availableMessageEffects), replyCount: dateReplies, + starsCount: starsCount, isPinned: message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread, hasAutoremove: message.isSelfExpiring, canViewReactionList: canViewMessageReactionList(message: message), diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index ffa7a26e05..6eb157593b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -130,7 +130,7 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ result.append((message, ChatMessageRestrictedBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) needReactions = false break outer - } else if let _ = attribute as? PaidStarsMessageAttribute, !addedPriceInfo { + } else if let _ = attribute as? PaidStarsMessageAttribute, !addedPriceInfo, message.id.peerId.namespace == Namespaces.Peer.CloudUser { result.append((message, ChatMessageActionBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) addedPriceInfo = true } @@ -299,7 +299,7 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ } if isMediaInverted { - result.insert((message, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: isFile ? .condensed : .default)), at: 0) + result.insert((message, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: isFile ? .condensed : .default)), at: addedPriceInfo ? 1 : 0) } else { result.append((message, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: isFile ? .condensed : .default))) needReactions = false @@ -327,7 +327,7 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ } if let attribute = message.attributes.first(where: { $0 is WebpagePreviewMessageAttribute }) as? WebpagePreviewMessageAttribute, attribute.leadingPreview { - result.insert((message, ChatMessageWebpageBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)), at: 0) + result.insert((message, ChatMessageWebpageBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)), at: addedPriceInfo ? 1 : 0) } else { result.append((message, ChatMessageWebpageBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) } @@ -362,7 +362,7 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ if result.isEmpty { needReactions = false } - result.insert((messageWithCaptionToAdd, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)), at: 0) + result.insert((messageWithCaptionToAdd, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)), at: addedPriceInfo ? 1 : 0) } else { result.append((messageWithCaptionToAdd, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) needReactions = false @@ -2276,6 +2276,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } var viewCount: Int? var dateReplies = 0 + var starsCount: Int64? var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeerId: item.context.account.peerId, accountPeer: item.associatedData.accountPeer, message: message) if message.isRestricted(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) { dateReactionsAndPeers = ([], []) @@ -2289,6 +2290,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .group = channel.info { dateReplies = Int(attribute.count) } + } else if let attribute = attribute as? PaidStarsMessageAttribute, item.message.id.peerId.namespace == Namespaces.Peer.CloudChannel { + starsCount = attribute.stars.value } } @@ -2337,6 +2340,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI areReactionsTags: item.message.areReactionsTags(accountPeerId: item.context.account.peerId), messageEffect: item.message.messageEffect(availableMessageEffects: item.associatedData.availableMessageEffects), replyCount: dateReplies, + starsCount: starsCount, isPinned: message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, hasAutoremove: message.isSelfExpiring, canViewReactionList: canViewMessageReactionList(message: message), diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageContactBubbleContentNode/Sources/ChatMessageContactBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageContactBubbleContentNode/Sources/ChatMessageContactBubbleContentNode.swift index e6156462fc..a64006553e 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageContactBubbleContentNode/Sources/ChatMessageContactBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageContactBubbleContentNode/Sources/ChatMessageContactBubbleContentNode.swift @@ -233,6 +233,7 @@ public class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { } var viewCount: Int? var dateReplies = 0 + var starsCount: Int64? var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeerId: item.context.account.peerId, accountPeer: item.associatedData.accountPeer, message: item.message) if item.message.isRestricted(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) { dateReactionsAndPeers = ([], []) @@ -246,6 +247,8 @@ public class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .group = channel.info { dateReplies = Int(attribute.count) } + } else if let attribute = attribute as? PaidStarsMessageAttribute, item.message.id.peerId.namespace == Namespaces.Peer.CloudChannel { + starsCount = attribute.stars.value } } @@ -300,6 +303,7 @@ public class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { areReactionsTags: item.topMessage.areReactionsTags(accountPeerId: item.context.account.peerId), messageEffect: messageEffect, replyCount: dateReplies, + starsCount: starsCount, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread, hasAutoremove: item.message.isSelfExpiring, canViewReactionList: canViewMessageReactionList(message: item.topMessage), diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift index 44698bdff0..5e9f63f0b6 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift @@ -195,6 +195,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { var areReactionsTags: Bool var messageEffect: AvailableMessageEffects.MessageEffect? var replyCount: Int + var starsCount: Int64? var isPinned: Bool var hasAutoremove: Bool var canViewReactionList: Bool @@ -218,6 +219,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { areReactionsTags: Bool, messageEffect: AvailableMessageEffects.MessageEffect?, replyCount: Int, + starsCount: Int64?, isPinned: Bool, hasAutoremove: Bool, canViewReactionList: Bool, @@ -240,6 +242,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { self.areReactionsTags = areReactionsTags self.messageEffect = messageEffect self.replyCount = replyCount + self.starsCount = starsCount self.isPinned = isPinned self.hasAutoremove = hasAutoremove self.canViewReactionList = canViewReactionList @@ -262,7 +265,9 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { private var repliesIcon: ASImageNode? private var selfExpiringIcon: ASImageNode? private var replyCountNode: TextNode? - + private var starsIcon: ASImageNode? + private var starsCountNode: TextNode? + private var type: ChatMessageDateAndStatusType? private var theme: ChatPresentationThemeData? private var layoutSize: CGSize? @@ -316,12 +321,14 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { var currentBackgroundNode = self.backgroundNode var currentImpressionIcon = self.impressionIcon var currentRepliesIcon = self.repliesIcon - + var currentStarsIcon = self.starsIcon + let currentType = self.type let currentTheme = self.theme let makeReplyCountLayout = TextNode.asyncLayout(self.replyCountNode) - + let makeStarsCountLayout = TextNode.asyncLayout(self.starsCountNode) + let reactionButtonsContainer = self.reactionButtonsContainer return { [weak self] arguments in @@ -337,7 +344,8 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { let clockMinImage: UIImage? var impressionImage: UIImage? var repliesImage: UIImage? - + var starsImage: UIImage? + let themeUpdated = arguments.presentationData.theme != currentTheme || arguments.type != currentType let graphics = PresentationResourcesChat.principalGraphics(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper, bubbleCorners: arguments.presentationData.chatBubbleCorners) @@ -404,6 +412,9 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { } else if arguments.isPinned { repliesImage = graphics.incomingDateAndStatusPinnedIcon } + if (arguments.starsCount ?? 0) != 0 { + starsImage = graphics.incomingDateAndStatusStarsIcon + } case let .BubbleOutgoing(status): dateColor = arguments.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor outgoingStatus = status @@ -420,6 +431,9 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { } else if arguments.isPinned { repliesImage = graphics.outgoingDateAndStatusPinnedIcon } + if (arguments.starsCount ?? 0) != 0 { + starsImage = graphics.outgoingDateAndStatusStarsIcon + } case .ImageIncoming: dateColor = arguments.presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor backgroundImage = graphics.dateAndStatusMediaBackground @@ -436,6 +450,9 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { } else if arguments.isPinned { repliesImage = graphics.mediaPinnedIcon } + if (arguments.starsCount ?? 0) != 0 { + starsImage = graphics.mediaStarsIcon + } case let .ImageOutgoing(status): dateColor = arguments.presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor outgoingStatus = status @@ -453,6 +470,9 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { } else if arguments.isPinned { repliesImage = graphics.mediaPinnedIcon } + if (arguments.starsCount ?? 0) != 0 { + starsImage = graphics.mediaStarsIcon + } case .FreeIncoming: let serviceColor = serviceMessageColorComponents(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper) dateColor = serviceColor.primaryText @@ -471,6 +491,9 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { } else if arguments.isPinned { repliesImage = graphics.freePinnedIcon } + if (arguments.starsCount ?? 0) != 0 { + starsImage = graphics.freeStarsIcon + } case let .FreeOutgoing(status): let serviceColor = serviceMessageColorComponents(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper) dateColor = serviceColor.primaryText @@ -489,6 +512,9 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { } else if arguments.isPinned { repliesImage = graphics.freePinnedIcon } + if (arguments.starsCount ?? 0) != 0 { + starsImage = graphics.freeStarsIcon + } } var updatedDateText = arguments.dateText @@ -541,6 +567,20 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { currentRepliesIcon = nil } + var starsIconSize = CGSize() + if let starsImage = starsImage { + if currentStarsIcon == nil { + let iconNode = ASImageNode() + iconNode.isLayerBacked = true + iconNode.displayWithoutProcessing = true + iconNode.displaysAsynchronously = false + currentStarsIcon = iconNode + } + starsIconSize = starsImage.size + } else { + currentStarsIcon = nil + } + if let outgoingStatus = outgoingStatus { switch outgoingStatus { case .Sending: @@ -652,7 +692,8 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { } var replyCountLayoutAndApply: (TextNodeLayout, () -> TextNode)? - + var starsCountLayoutAndApply: (TextNodeLayout, () -> TextNode)? + let reactionSize: CGFloat = 8.0 let reactionSpacing: CGFloat = 2.0 let reactionTrailingSpacing: CGFloat = 6.0 @@ -671,11 +712,29 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { let layoutAndApply = makeReplyCountLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: countString, font: dateFont, textColor: dateColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0))) reactionInset += 14.0 + layoutAndApply.0.size.width + 4.0 + if arguments.starsCount != nil { + reactionInset += 3.0 + } replyCountLayoutAndApply = layoutAndApply } else if arguments.isPinned { reactionInset += 12.0 } + if let starsCount = arguments.starsCount, starsCount > 0 { + let countString: String + if starsCount > 1000000 { + countString = "\(starsCount / 1000000)M" + } else if starsCount > 1000 { + countString = "\(starsCount / 1000)K" + } else { + countString = "\(starsCount)" + } + + let layoutAndApply = makeStarsCountLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: countString, font: dateFont, textColor: dateColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0))) + reactionInset += 14.0 + layoutAndApply.0.size.width + 4.0 + starsCountLayoutAndApply = layoutAndApply + } + if arguments.messageEffect != nil { reactionInset += 13.0 } @@ -1227,6 +1286,9 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { let replyCountFrame = CGRect(origin: CGPoint(x: reactionOffset + 4.0, y: backgroundInsets.top + 1.0 + offset + verticalInset), size: layout.size) animation.animator.updateFrame(layer: node.layer, frame: replyCountFrame, completion: nil) reactionOffset += 4.0 + layout.size.width + if currentStarsIcon != nil { + reactionOffset += 8.0 + } } else if let replyCountNode = strongSelf.replyCountNode { strongSelf.replyCountNode = nil if animation.isAnimated { @@ -1237,6 +1299,56 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { replyCountNode.removeFromSupernode() } } + + if let currentStarsIcon = currentStarsIcon { + currentStarsIcon.displaysAsynchronously = false + if currentStarsIcon.image !== starsImage { + currentStarsIcon.image = starsImage + } + if currentStarsIcon.supernode == nil { + strongSelf.starsIcon = currentStarsIcon + strongSelf.addSubnode(currentStarsIcon) + if animation.isAnimated { + currentStarsIcon.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + } + } + let starsIconFrame = CGRect(origin: CGPoint(x: reactionOffset - 2.0, y: backgroundInsets.top + offset + verticalInset + floor((date.size.height - starsIconSize.height) / 2.0)), size: starsIconSize) + animation.animator.updateFrame(layer: currentStarsIcon.layer, frame: starsIconFrame, completion: nil) + reactionOffset += 9.0 + } else if let starsIcon = strongSelf.starsIcon { + strongSelf.starsIcon = nil + if animation.isAnimated { + starsIcon.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak starsIcon] _ in + starsIcon?.removeFromSupernode() + }) + } else { + starsIcon.removeFromSupernode() + } + } + + if let (layout, apply) = starsCountLayoutAndApply { + let node = apply() + if strongSelf.starsCountNode !== node { + strongSelf.starsCountNode?.removeFromSupernode() + strongSelf.addSubnode(node) + strongSelf.starsCountNode = node + if animation.isAnimated { + node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + } + } + let starsCountFrame = CGRect(origin: CGPoint(x: reactionOffset + 4.0, y: backgroundInsets.top + 1.0 + offset + verticalInset), size: layout.size) + animation.animator.updateFrame(layer: node.layer, frame: starsCountFrame, completion: nil) + reactionOffset += 4.0 + layout.size.width + } else if let starsCountNode = strongSelf.starsCountNode { + strongSelf.starsCountNode = nil + if animation.isAnimated { + starsCountNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak starsCountNode] _ in + starsCountNode?.removeFromSupernode() + }) + } else { + starsCountNode.removeFromSupernode() + } + } } }) }) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageFactCheckBubbleContentNode/Sources/ChatMessageFactCheckBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageFactCheckBubbleContentNode/Sources/ChatMessageFactCheckBubbleContentNode.swift index a2e2b5deef..a41e7a8686 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageFactCheckBubbleContentNode/Sources/ChatMessageFactCheckBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageFactCheckBubbleContentNode/Sources/ChatMessageFactCheckBubbleContentNode.swift @@ -295,6 +295,7 @@ public class ChatMessageFactCheckBubbleContentNode: ChatMessageBubbleContentNode var rawText = "" var rawEntities: [MessageTextEntity] = [] var dateReplies = 0 + var starsCount: Int64? var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeerId: item.context.account.peerId, accountPeer: item.associatedData.accountPeer, message: item.message) if item.message.isRestricted(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) { dateReactionsAndPeers = ([], []) @@ -311,6 +312,8 @@ public class ChatMessageFactCheckBubbleContentNode: ChatMessageBubbleContentNode if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .group = channel.info { dateReplies = Int(attribute.count) } + } else if let attribute = attribute as? PaidStarsMessageAttribute, item.message.id.peerId.namespace == Namespaces.Peer.CloudChannel { + starsCount = attribute.stars.value } } @@ -447,6 +450,7 @@ public class ChatMessageFactCheckBubbleContentNode: ChatMessageBubbleContentNode areReactionsTags: item.topMessage.areReactionsTags(accountPeerId: item.context.account.peerId), messageEffect: item.topMessage.messageEffect(availableMessageEffects: item.associatedData.availableMessageEffects), replyCount: dateReplies, + starsCount: starsCount, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread, hasAutoremove: item.message.isSelfExpiring, canViewReactionList: canViewMessageReactionList(message: item.topMessage), diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift index e939dc18e6..263522c344 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift @@ -578,6 +578,7 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode, areReactionsTags: item.topMessage.areReactionsTags(accountPeerId: item.context.account.peerId), messageEffect: item.topMessage.messageEffect(availableMessageEffects: item.associatedData.availableMessageEffects), replyCount: dateReplies, + starsCount: nil, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread, hasAutoremove: item.message.isSelfExpiring, canViewReactionList: canViewMessageReactionList(message: item.topMessage), diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift index 5357300699..88c6dd9b03 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift @@ -898,6 +898,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { } var viewCount: Int? var dateReplies = 0 + var starsCount: Int64? var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeerId: arguments.context.account.peerId, accountPeer: arguments.associatedData.accountPeer, message: arguments.topMessage) if arguments.topMessage.isRestricted(platform: "ios", contentSettings: arguments.context.currentContentSettings.with { $0 }) || arguments.presentationData.isPreview { dateReactionsAndPeers = ([], []) @@ -911,6 +912,8 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { if let channel = arguments.message.peers[arguments.message.id.peerId] as? TelegramChannel, case .group = channel.info { dateReplies = Int(attribute.count) } + } else if let attribute = attribute as? PaidStarsMessageAttribute, arguments.message.id.peerId.namespace == Namespaces.Peer.CloudChannel { + starsCount = attribute.stars.value } } if arguments.forcedIsEdited { @@ -956,6 +959,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { areReactionsTags: arguments.message.areReactionsTags(accountPeerId: arguments.context.account.peerId), messageEffect: arguments.message.messageEffect(availableMessageEffects: arguments.associatedData.availableMessageEffects), replyCount: dateReplies, + starsCount: starsCount, isPinned: arguments.isPinned && !arguments.associatedData.isInPinnedListMode, hasAutoremove: arguments.message.isSelfExpiring, canViewReactionList: canViewMessageReactionList(message: arguments.topMessage), diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift index a55f886df6..f95da1047c 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift @@ -524,6 +524,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { let sentViaBot = false var viewCount: Int? = nil var dateReplies = 0 + var starsCount: Int64? var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeerId: item.context.account.peerId, accountPeer: item.associatedData.accountPeer, message: item.message) if item.message.isRestricted(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) { dateReactionsAndPeers = ([], []) @@ -537,6 +538,8 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .group = channel.info { dateReplies = Int(attribute.count) } + } else if let attribute = attribute as? PaidStarsMessageAttribute, item.message.id.peerId.namespace == Namespaces.Peer.CloudChannel { + starsCount = attribute.stars.value } } @@ -583,6 +586,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { areReactionsTags: item.topMessage.areReactionsTags(accountPeerId: item.context.account.peerId), messageEffect: messageEffect, replyCount: dateReplies, + starsCount: starsCount, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, hasAutoremove: item.message.isSelfExpiring, canViewReactionList: canViewMessageReactionList(message: item.topMessage), diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift index 4b569bba1e..5bb87a3def 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift @@ -83,6 +83,7 @@ public struct ChatMessageDateAndStatus { public var dateReactions: [MessageReaction] public var dateReactionPeers: [(MessageReaction.Reaction, EnginePeer)] public var dateReplies: Int + public var starsCount: Int64? public var isPinned: Bool public var dateText: String @@ -93,6 +94,7 @@ public struct ChatMessageDateAndStatus { dateReactions: [MessageReaction], dateReactionPeers: [(MessageReaction.Reaction, EnginePeer)], dateReplies: Int, + starsCount: Int64?, isPinned: Bool, dateText: String ) { @@ -102,6 +104,7 @@ public struct ChatMessageDateAndStatus { self.dateReactions = dateReactions self.dateReactionPeers = dateReactionPeers self.dateReplies = dateReplies + self.starsCount = starsCount self.isPinned = isPinned self.dateText = dateText } @@ -1118,6 +1121,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr areReactionsTags: message.areReactionsTags(accountPeerId: context.account.peerId), messageEffect: messageEffect, replyCount: dateAndStatus.dateReplies, + starsCount: dateAndStatus.starsCount, isPinned: dateAndStatus.isPinned, hasAutoremove: message.isSelfExpiring, canViewReactionList: canViewMessageReactionList(message: message), diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift index 2c8c18860a..a9511b99c1 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift @@ -192,6 +192,7 @@ public class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { } var viewCount: Int? var dateReplies = 0 + var starsCount: Int64? var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeerId: item.context.account.peerId, accountPeer: item.associatedData.accountPeer, message: item.message) if item.message.isRestricted(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) { dateReactionsAndPeers = ([], []) @@ -205,6 +206,8 @@ public class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .group = channel.info { dateReplies = Int(attribute.count) } + } else if let attribute = attribute as? PaidStarsMessageAttribute, item.message.id.peerId.namespace == Namespaces.Peer.CloudChannel { + starsCount = attribute.stars.value } } @@ -284,6 +287,7 @@ public class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { areReactionsTags: item.topMessage.areReactionsTags(accountPeerId: item.context.account.peerId), messageEffect: item.topMessage.messageEffect(availableMessageEffects: item.associatedData.availableMessageEffects), replyCount: dateReplies, + starsCount: starsCount, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, hasAutoremove: item.message.isSelfExpiring, canViewReactionList: canViewMessageReactionList(message: item.topMessage), diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift index 49392be769..8f8397d26e 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift @@ -307,6 +307,7 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { } var viewCount: Int? var dateReplies = 0 + var starsCount: Int64? var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeerId: item.context.account.peerId, accountPeer: item.associatedData.accountPeer, message: item.message) if item.message.isRestricted(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) { dateReactionsAndPeers = ([], []) @@ -323,6 +324,8 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .group = channel.info { dateReplies = Int(attribute.count) } + } else if let attribute = attribute as? PaidStarsMessageAttribute, item.message.id.peerId.namespace == Namespaces.Peer.CloudChannel { + starsCount = attribute.stars.value } } @@ -373,6 +376,7 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { dateReactions: dateReactionsAndPeers.reactions, dateReactionPeers: dateReactionsAndPeers.peers, dateReplies: dateReplies, + starsCount: starsCount, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, dateText: dateText ) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessagePaymentAlertController/Sources/ChatMessagePaymentAlertController.swift b/submodules/TelegramUI/Components/Chat/ChatMessagePaymentAlertController/Sources/ChatMessagePaymentAlertController.swift index 151913193d..75270b57dc 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessagePaymentAlertController/Sources/ChatMessagePaymentAlertController.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessagePaymentAlertController/Sources/ChatMessagePaymentAlertController.swift @@ -317,18 +317,20 @@ private final class ChatMessagePaymentAlertContentNode: AlertContentNode, ASGest } } -private class ChatMessagePaymentAlertController: AlertController { +public class ChatMessagePaymentAlertController: AlertController { private let context: AccountContext? private let presentationData: PresentationData private weak var parentNavigationController: NavigationController? - + private let showBalance: Bool + private let balance = ComponentView() - init(context: AccountContext?, presentationData: PresentationData, contentNode: AlertContentNode, navigationController: NavigationController?) { + public init(context: AccountContext?, presentationData: PresentationData, contentNode: AlertContentNode, navigationController: NavigationController?, showBalance: Bool = true) { self.context = context self.presentationData = presentationData self.parentNavigationController = navigationController - + self.showBalance = showBalance + super.init(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode) self.willDismiss = { [weak self] in @@ -350,16 +352,16 @@ private class ChatMessagePaymentAlertController: AlertController { } } - override func dismissAnimated() { + public override func dismissAnimated() { super.dismissAnimated() self.animateOut() } - override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - if let context = self.context, let _ = self.parentNavigationController { + if let context = self.context, let _ = self.parentNavigationController, self.showBalance { let insets = layout.insets(options: .statusBar) let balanceSize = self.balance.update( transition: .immediate, diff --git a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift index 332531f153..0017cedcf4 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift @@ -1054,6 +1054,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } var viewCount: Int? var dateReplies = 0 + var starsCount: Int64? var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeerId: item.context.account.peerId, accountPeer: item.associatedData.accountPeer, message: item.message) if item.message.isRestricted(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) { dateReactionsAndPeers = ([], []) @@ -1067,6 +1068,8 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .group = channel.info { dateReplies = Int(attribute.count) } + } else if let attribute = attribute as? PaidStarsMessageAttribute, item.message.id.peerId.namespace == Namespaces.Peer.CloudChannel { + starsCount = attribute.stars.value } } @@ -1125,6 +1128,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { areReactionsTags: item.topMessage.areReactionsTags(accountPeerId: item.context.account.peerId), messageEffect: item.topMessage.messageEffect(availableMessageEffects: item.associatedData.availableMessageEffects), replyCount: dateReplies, + starsCount: starsCount, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, hasAutoremove: item.message.isSelfExpiring, canViewReactionList: canViewMessageReactionList(message: item.topMessage), diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageRestrictedBubbleContentNode/Sources/ChatMessageRestrictedBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageRestrictedBubbleContentNode/Sources/ChatMessageRestrictedBubbleContentNode.swift index 8d5e633293..01a336ad60 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageRestrictedBubbleContentNode/Sources/ChatMessageRestrictedBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageRestrictedBubbleContentNode/Sources/ChatMessageRestrictedBubbleContentNode.swift @@ -56,6 +56,7 @@ public class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNod var viewCount: Int? var rawText = "" var dateReplies = 0 + var starsCount: Int64? var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeerId: item.context.account.peerId, accountPeer: item.associatedData.accountPeer, message: item.message) if item.message.isRestricted(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) { dateReactionsAndPeers = ([], []) @@ -71,6 +72,8 @@ public class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNod if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .group = channel.info { dateReplies = Int(attribute.count) } + } else if let attribute = attribute as? PaidStarsMessageAttribute, item.message.id.peerId.namespace == Namespaces.Peer.CloudChannel { + starsCount = attribute.stars.value } } @@ -140,6 +143,7 @@ public class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNod areReactionsTags: item.topMessage.areReactionsTags(accountPeerId: item.context.account.peerId), messageEffect: item.topMessage.messageEffect(availableMessageEffects: item.associatedData.availableMessageEffects), replyCount: dateReplies, + starsCount: starsCount, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread, hasAutoremove: item.message.isSelfExpiring, canViewReactionList: canViewMessageReactionList(message: item.topMessage), diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift index c968aa1a50..e2d44addfe 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift @@ -602,6 +602,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { var edited = false var viewCount: Int? = nil var dateReplies = 0 + var starsCount: Int64? var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeerId: item.context.account.peerId, accountPeer: item.associatedData.accountPeer, message: item.message) if item.message.isRestricted(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) { dateReactionsAndPeers = ([], []) @@ -615,6 +616,8 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .group = channel.info { dateReplies = Int(attribute.count) } + } else if let attribute = attribute as? PaidStarsMessageAttribute, item.message.id.peerId.namespace == Namespaces.Peer.CloudChannel { + starsCount = attribute.stars.value } } @@ -648,6 +651,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { areReactionsTags: item.message.areReactionsTags(accountPeerId: item.context.account.peerId), messageEffect: item.message.messageEffect(availableMessageEffects: item.associatedData.availableMessageEffects), replyCount: dateReplies, + starsCount: starsCount, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, hasAutoremove: item.message.isSelfExpiring, canViewReactionList: canViewMessageReactionList(message: item.message), diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift index f6e820a9af..1e0dd952d7 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift @@ -264,6 +264,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } var viewCount: Int? var dateReplies = 0 + var starsCount: Int64? var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeerId: item.context.account.peerId, accountPeer: item.associatedData.accountPeer, message: item.topMessage) if item.message.isRestricted(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) { dateReactionsAndPeers = ([], []) @@ -278,6 +279,8 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .group = channel.info { dateReplies = Int(attribute.count) } + } else if let attribute = attribute as? PaidStarsMessageAttribute, item.message.id.peerId.namespace == Namespaces.Peer.CloudChannel { + starsCount = attribute.stars.value } } @@ -647,6 +650,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { areReactionsTags: item.topMessage.areReactionsTags(accountPeerId: item.context.account.peerId), messageEffect: item.topMessage.messageEffect(availableMessageEffects: item.associatedData.availableMessageEffects), replyCount: dateReplies, + starsCount: starsCount, isPinned: item.message.tags.contains(.pinned) && (!item.associatedData.isInPinnedListMode || isReplyThread), hasAutoremove: item.message.isSelfExpiring, canViewReactionList: canViewMessageReactionList(message: item.topMessage), diff --git a/submodules/TelegramUI/Components/Chat/ChatUserInfoItem/Sources/ChatUserInfoItem.swift b/submodules/TelegramUI/Components/Chat/ChatUserInfoItem/Sources/ChatUserInfoItem.swift index 9bf11f830f..c7beb06383 100644 --- a/submodules/TelegramUI/Components/Chat/ChatUserInfoItem/Sources/ChatUserInfoItem.swift +++ b/submodules/TelegramUI/Components/Chat/ChatUserInfoItem/Sources/ChatUserInfoItem.swift @@ -393,7 +393,7 @@ public final class ChatUserInfoItemNode: ListViewItemNode, ASGestureRecognizerDe let disclaimerText: NSMutableAttributedString if let verification = item.verification { - disclaimerText = NSMutableAttributedString(string: " # \(verification.description)", font: Font.regular(13.0), textColor: subtitleColor) + disclaimerText = NSMutableAttributedString(string: " # \(verification.description)", font: Font.regular(13.0), textColor: subtitleColor) if let range = disclaimerText.string.range(of: "#") { disclaimerText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: verification.iconFileId, file: nil), range: NSRange(range, in: disclaimerText.string)) disclaimerText.addAttribute(.foregroundColor, value: subtitleColor, range: NSRange(range, in: disclaimerText.string)) diff --git a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift index b684b61345..6c8266f75c 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift @@ -216,6 +216,8 @@ final class GiftOptionsScreenComponent: Component { private var starsStateDisposable: Disposable? private var starsState: StarsContext.State? + private let optionsPromise = Promise<[StarsTopUpOption]?>(nil) + private var component: GiftOptionsScreenComponent? private(set) weak var state: State? private var environment: EnvironmentType? @@ -508,59 +510,94 @@ final class GiftOptionsScreenComponent: Component { gift: transferGift, peer: peer, transferStars: gift.transferStars ?? 0, - commit: { [weak controller] in - let _ = (context.engine.payments.transferStarGift(prepaid: gift.transferStars == 0, reference: reference, peerId: peer.id) - |> deliverOnMainQueue).start() - - guard let controller, let navigationController = controller.navigationController as? NavigationController else { - return - } - - if peer.id.namespace == Namespaces.Peer.CloudChannel { - var controllers = navigationController.viewControllers - controllers = controllers.filter { !($0 is GiftSetupScreen) && !($0 is GiftOptionsScreenProtocol) } - var foundController = false - for controller in controllers.reversed() { - if let controller = controller as? PeerInfoScreen, controller.peerId == component.peerId { - foundController = true - break - } + navigationController: controller.navigationController as? NavigationController, + commit: { [weak self, weak controller] in + let proceed: (Bool) -> Void = { waitForTopUp in + if waitForTopUp, let starsContext = context.starsContext { + let _ = (starsContext.onUpdate + |> deliverOnMainQueue).start(next: { + let _ = (context.engine.payments.transferStarGift(prepaid: gift.transferStars == 0, reference: reference, peerId: peer.id) + |> deliverOnMainQueue).start() + }) + } else { + let _ = (context.engine.payments.transferStarGift(prepaid: gift.transferStars == 0, reference: reference, peerId: peer.id) + |> deliverOnMainQueue).start() } - if !foundController { - if let controller = context.sharedContext.makePeerInfoController( - context: context, - updatedPresentationData: nil, - peer: peer._asPeer(), - mode: .gifts, - avatarInitiallyExpanded: false, - fromChat: false, - requestsContext: nil - ) { - controllers.append(controller) - } + + guard let controller, let navigationController = controller.navigationController as? NavigationController else { + return } - navigationController.setViewControllers(controllers, animated: true) - } else { - var controllers = navigationController.viewControllers - controllers = controllers.filter { !($0 is GiftSetupScreen) && !($0 is GiftOptionsScreenProtocol) && !($0 is PeerInfoScreen) && !($0 is ContactSelectionController) } - var foundController = false - for controller in controllers.reversed() { - if let chatController = controller as? ChatController, case .peer(id: component.peerId) = chatController.chatLocation { + + if peer.id.namespace == Namespaces.Peer.CloudChannel { + var controllers = navigationController.viewControllers + controllers = controllers.filter { !($0 is GiftSetupScreen) && !($0 is GiftOptionsScreenProtocol) } + var foundController = false + for controller in controllers.reversed() { + if let controller = controller as? PeerInfoScreen, controller.peerId == component.peerId { + foundController = true + break + } + } + if !foundController { + if let controller = context.sharedContext.makePeerInfoController( + context: context, + updatedPresentationData: nil, + peer: peer._asPeer(), + mode: .gifts, + avatarInitiallyExpanded: false, + fromChat: false, + requestsContext: nil + ) { + controllers.append(controller) + } + } + navigationController.setViewControllers(controllers, animated: true) + } else { + var controllers = navigationController.viewControllers + controllers = controllers.filter { !($0 is GiftSetupScreen) && !($0 is GiftOptionsScreenProtocol) && !($0 is PeerInfoScreen) && !($0 is ContactSelectionController) } + var foundController = false + for controller in controllers.reversed() { + if let chatController = controller as? ChatController, case .peer(id: component.peerId) = chatController.chatLocation { + chatController.hintPlayNextOutgoingGift() + foundController = true + break + } + } + if !foundController { + let chatController = component.context.sharedContext.makeChatController(context: component.context, chatLocation: .peer(id: component.peerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil) chatController.hintPlayNextOutgoingGift() - foundController = true - break + controllers.append(chatController) } + navigationController.setViewControllers(controllers, animated: true) } - if !foundController { - let chatController = component.context.sharedContext.makeChatController(context: component.context, chatLocation: .peer(id: component.peerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil) - chatController.hintPlayNextOutgoingGift() - controllers.append(chatController) + if let completion = component.completion { + completion() } - navigationController.setViewControllers(controllers, animated: true) } - - if let completion = component.completion { - completion() + + if let self, let transferStars = gift.transferStars, transferStars > 0, let starsContext = context.starsContext, let starsState = self.starsState { + if starsState.balance < StarsAmount(value: transferStars, nanos: 0) { + let _ = (self.optionsPromise.get() + |> filter { $0 != nil } + |> take(1) + |> deliverOnMainQueue).startStandalone(next: { [weak controller] options in + let purchaseController = context.sharedContext.makeStarsPurchaseScreen( + context: context, + starsContext: starsContext, + options: options ?? [], + purpose: .transferStarGift(requiredStars: transferStars), + completion: { stars in + starsContext.add(balance: StarsAmount(value: stars, nanos: 0)) + proceed(true) + } + ) + controller?.push(purchaseController) + }) + } else { + proceed(false) + } + } else { + proceed(false) } } ) @@ -590,6 +627,11 @@ final class GiftOptionsScreenComponent: Component { self.state?.updated() } }) + + if let state = component.starsContext.currentState, state.balance < StarsAmount(value: 100, nanos: 0) { + self.optionsPromise.set(component.context.engine.payments.starsTopUpOptions() + |> map(Optional.init)) + } } self.component = component @@ -1237,6 +1279,10 @@ final class GiftOptionsScreenComponent: Component { if availableProducts.isEmpty { var premiumProducts: [PremiumGiftProduct] = [] for option in premiumOptions { + if option.currency == "XTR" { + continue + } + let starsGiftOption = premiumOptions.first(where: { $0.currency == "XTR" && $0.months == option.months }) premiumProducts.append( PremiumGiftProduct( giftOption: CachedPremiumGiftOption( @@ -1246,7 +1292,7 @@ final class GiftOptionsScreenComponent: Component { botUrl: "", storeProductId: option.storeProductId ), - starsGiftOption: nil, + starsGiftOption: starsGiftOption, storeProduct: nil, discount: nil ) diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift index 56549057c4..ece1fa0f97 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift @@ -498,16 +498,8 @@ final class GiftSetupScreenComponent: Component { starsContext.add(balance: StarsAmount(value: stars, nanos: 0)) - let _ = (starsContext.state - |> take(until: { value in - if let value { - if !value.flags.contains(.isPendingBalance) { - return SignalTakeAction(passthrough: true, complete: true) - } - } - return SignalTakeAction(passthrough: false, complete: false) - }) - |> deliverOnMainQueue).start(next: { _ in + let _ = (starsContext.onUpdate + |> deliverOnMainQueue).start(next: { proceed() }) } diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD index ace4e1f471..66375077f3 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD @@ -49,6 +49,7 @@ swift_library( "//submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController", "//submodules/TelegramUI/Components/PremiumLockButtonSubtitleComponent", "//submodules/TelegramUI/Components/Stars/StarsBalanceOverlayComponent", + "//submodules/TelegramUI/Components/Chat/ChatMessagePaymentAlertController", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftTransferAlertController.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftTransferAlertController.swift index ffa0d3ffb8..91ccd670ac 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftTransferAlertController.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftTransferAlertController.swift @@ -12,6 +12,7 @@ import AppBundle import AvatarNode import Markdown import GiftItemComponent +import ChatMessagePaymentAlertController private final class GiftTransferAlertContentNode: AlertContentNode { private let context: AccountContext @@ -251,7 +252,14 @@ private final class GiftTransferAlertContentNode: AlertContentNode { } } -public func giftTransferAlertController(context: AccountContext, gift: StarGift.UniqueGift, peer: EnginePeer, transferStars: Int64, commit: @escaping () -> Void) -> AlertController { +public func giftTransferAlertController( + context: AccountContext, + gift: StarGift.UniqueGift, + peer: EnginePeer, + transferStars: Int64, + navigationController: NavigationController?, + commit: @escaping () -> Void +) -> AlertController { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let strings = presentationData.strings @@ -267,7 +275,6 @@ public func giftTransferAlertController(context: AccountContext, gift: StarGift. } var dismissImpl: ((Bool) -> Void)? - var contentNode: GiftTransferAlertContentNode? let actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: buttonText, action: { dismissImpl?(true) commit() @@ -275,9 +282,9 @@ public func giftTransferAlertController(context: AccountContext, gift: StarGift. dismissImpl?(true) })] - contentNode = GiftTransferAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: strings, gift: gift, peer: peer, title: title, text: text, actions: actions) + let contentNode = GiftTransferAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: strings, gift: gift, peer: peer, title: title, text: text, actions: actions) - let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode!) + let controller = ChatMessagePaymentAlertController(context: context, presentationData: presentationData, contentNode: contentNode, navigationController: navigationController, showBalance: transferStars > 0) dismissImpl = { [weak controller] animated in if animated { controller?.dismissAnimated() diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index 1ca31db8c7..469950558b 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -147,13 +147,7 @@ private final class GiftViewSheetContent: CombinedComponent { var keepOriginalInfo = false - private var optionsDisposable: Disposable? - private(set) var options: [StarsTopUpOption] = [] { - didSet { - self.optionsPromise.set(self.options) - } - } - private let optionsPromise = ValuePromise<[StarsTopUpOption]?>(nil) + private let optionsPromise = Promise<[StarsTopUpOption]?>(nil) init( context: AccountContext, @@ -269,13 +263,8 @@ private final class GiftViewSheetContent: CombinedComponent { } if let starsContext = context.starsContext, let state = starsContext.currentState, state.balance < StarsAmount(value: 100, nanos: 0) { - self.optionsDisposable = (context.engine.payments.starsTopUpOptions() - |> deliverOnMainQueue).start(next: { [weak self] options in - guard let self else { - return - } - self.options = options - }) + self.optionsPromise.set(context.engine.payments.starsTopUpOptions() + |> map(Optional.init)) } } @@ -351,8 +340,12 @@ private final class GiftViewSheetContent: CombinedComponent { self.inProgress = true self.updated() + if let controller = self.getController() as? GiftViewScreen { + controller.showBalance = false + } + self.upgradeDisposable = (self.upgradeGift(formId, self.keepOriginalInfo) - |> deliverOnMainQueue).start(next: { [weak self] result in + |> deliverOnMainQueue).start(next: { [weak self, weak starsContext] result in guard let self, let controller = self.getController() as? GiftViewScreen else { return } @@ -363,6 +356,10 @@ private final class GiftViewSheetContent: CombinedComponent { controller.subject = self.subject controller.animateSuccess() self.updated(transition: .spring(duration: 0.4)) + + Queue.mainQueue().after(0.5) { + starsContext?.load(force: true) + } }) } @@ -382,11 +379,18 @@ private final class GiftViewSheetContent: CombinedComponent { starsContext: starsContext, options: options ?? [], purpose: .upgradeStarGift(requiredStars: price), - completion: { [weak starsContext] stars in - starsContext?.add(balance: StarsAmount(value: stars, nanos: 0)) - Queue.mainQueue().after(2.0) { - proceed(upgradeForm.id) + completion: { [weak self, weak starsContext] stars in + guard let self, let starsContext else { + return } + self.inProgress = true + self.updated() + + starsContext.add(balance: StarsAmount(value: stars, nanos: 0)) + let _ = (starsContext.onUpdate + |> deliverOnMainQueue).start(next: { + proceed(upgradeForm.id) + }) } ) controller.push(purchaseController) @@ -2226,9 +2230,8 @@ private final class GiftViewSheetContent: CombinedComponent { .position(CGPoint(x: context.availableSize.width - environment.safeInsets.left - 16.0 - buttons.size.width / 2.0, y: 28.0)) ) - let contentSize = CGSize(width: context.availableSize.width, height: originY + 5.0 + environment.safeInsets.bottom) - - return contentSize + let effectiveBottomInset: CGFloat = environment.metrics.isTablet ? 0.0 : environment.safeInsets.bottom + return CGSize(width: context.availableSize.width, height: originY + 5.0 + effectiveBottomInset) } } } diff --git a/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/BUILD b/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/BUILD index a5ce7cf0f9..7f66e412c3 100644 --- a/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/BUILD @@ -28,7 +28,6 @@ swift_library( "//submodules/TelegramUI/Components/ListSectionComponent", "//submodules/TelegramUI/Components/ListItemSliderSelectorComponent", "//submodules/TelegramUI/Components/ListActionItemComponent", - "//submodules/PremiumUI", "//submodules/Components/BlurredBackgroundComponent", "//submodules/Markdown", "//submodules/PresentationDataUtils", @@ -38,6 +37,7 @@ swift_library( "//submodules/TelegramUI/Components/PlainButtonComponent", "//submodules/TelegramUI/Components/ToastComponent", "//submodules/AvatarNode", + "//submodules/TelegramUI/Components/Premium/PremiumCoinComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/AffiliateProgramSetupScreen.swift b/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/AffiliateProgramSetupScreen.swift index 7a48e4fd5f..c27262b53c 100644 --- a/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/AffiliateProgramSetupScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/AffiliateProgramSetupScreen.swift @@ -19,13 +19,13 @@ import ListItemSliderSelectorComponent import ListActionItemComponent import Markdown import BlurredBackgroundComponent -import PremiumUI import PresentationDataUtils import PeerListItemComponent import TelegramStringFormatting import ContextUI import BalancedTextComponent import AlertComponent +import PremiumCoinComponent private func textForTimeout(value: Int32) -> String { if value < 3600 { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift index baf1702c7e..09faa26854 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift @@ -111,6 +111,7 @@ public final class PeerInfoCoverComponent: Component { public let files: [Int64: TelegramMediaFile] public let isDark: Bool public let avatarCenter: CGPoint + public let avatarSize: CGSize public let avatarScale: CGFloat public let defaultHeight: CGFloat public let gradientOnTop: Bool @@ -124,6 +125,7 @@ public final class PeerInfoCoverComponent: Component { files: [Int64: TelegramMediaFile], isDark: Bool, avatarCenter: CGPoint, + avatarSize: CGSize = CGSize(width: 100.0, height: 100.0), avatarScale: CGFloat, defaultHeight: CGFloat, gradientOnTop: Bool = false, @@ -136,6 +138,7 @@ public final class PeerInfoCoverComponent: Component { self.files = files self.isDark = isDark self.avatarCenter = avatarCenter + self.avatarSize = avatarSize self.avatarScale = avatarScale self.defaultHeight = defaultHeight self.gradientOnTop = gradientOnTop @@ -160,6 +163,9 @@ public final class PeerInfoCoverComponent: Component { if lhs.avatarCenter != rhs.avatarCenter { return false } + if lhs.avatarSize != rhs.avatarSize { + return false + } if lhs.avatarScale != rhs.avatarScale { return false } @@ -492,7 +498,7 @@ public final class PeerInfoCoverComponent: Component { transition.containedViewLayoutTransition.updateFrameAdditive(view: self.backgroundPatternContainer, frame: backgroundPatternContainerFrame) transition.setAlpha(view: self.backgroundPatternContainer, alpha: component.patternTransitionFraction) - var baseDistance: CGFloat = 72.0 + var baseDistance: CGFloat = component.avatarSize.width / 2.0 + 22.0 var baseRowDistance: CGFloat = 28.0 var baseItemSize: CGFloat = 26.0 if availableSize.width <= 60.0 { @@ -516,7 +522,7 @@ public final class PeerInfoCoverComponent: Component { let baseItemDistance: CGFloat = baseDistance + CGFloat(row) * baseRowDistance let itemDistanceFraction = max(0.0, min(1.0, baseItemDistance / (baseDistance * 2.0))) - let itemScaleFraction = patternScaleValueAt(fraction: component.avatarTransitionFraction, t: itemDistanceFraction, reverse: false) + let itemScaleFraction = patternScaleValueAt(fraction: component.avatarTransitionFraction * 1.6, t: itemDistanceFraction, reverse: false) let itemDistance = baseItemDistance * (1.0 - itemScaleFraction) + 20.0 * itemScaleFraction var itemAngle: CGFloat diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoGiftsCoverComponent.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoGiftsCoverComponent.swift index 6c81f40c57..66f1bba085 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoGiftsCoverComponent.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoGiftsCoverComponent.swift @@ -19,6 +19,7 @@ public final class PeerInfoGiftsCoverComponent: Component { public let giftsContext: ProfileGiftsContext public let hasBackground: Bool public let avatarCenter: CGPoint + public let avatarSize: CGSize public let defaultHeight: CGFloat public let avatarTransitionFraction: CGFloat public let statusBarHeight: CGFloat @@ -34,6 +35,7 @@ public final class PeerInfoGiftsCoverComponent: Component { giftsContext: ProfileGiftsContext, hasBackground: Bool, avatarCenter: CGPoint, + avatarSize: CGSize, defaultHeight: CGFloat, avatarTransitionFraction: CGFloat, statusBarHeight: CGFloat, @@ -48,6 +50,7 @@ public final class PeerInfoGiftsCoverComponent: Component { self.giftsContext = giftsContext self.hasBackground = hasBackground self.avatarCenter = avatarCenter + self.avatarSize = avatarSize self.defaultHeight = defaultHeight self.avatarTransitionFraction = avatarTransitionFraction self.statusBarHeight = statusBarHeight @@ -71,6 +74,9 @@ public final class PeerInfoGiftsCoverComponent: Component { if lhs.avatarCenter != rhs.avatarCenter { return false } + if lhs.avatarSize != rhs.avatarSize { + return false + } if lhs.defaultHeight != rhs.defaultHeight { return false } @@ -213,14 +219,14 @@ public final class PeerInfoGiftsCoverComponent: Component { } excludeRects.append(CGRect(origin: CGPoint(x: 0.0, y: component.statusBarHeight), size: component.topLeftButtonsSize)) excludeRects.append(CGRect(origin: CGPoint(x: availableSize.width - component.topRightButtonsSize.width, y: component.statusBarHeight), size: component.topRightButtonsSize)) - excludeRects.append(CGRect(origin: CGPoint(x: floor((availableSize.width - component.titleWidth) / 2.0), y: avatarCenter.y + 56.0), size: CGSize(width: component.titleWidth, height: 72.0))) + excludeRects.append(CGRect(origin: CGPoint(x: floor((availableSize.width - component.titleWidth) / 2.0), y: avatarCenter.y + component.avatarSize.height / 2.0 + 6.0), size: CGSize(width: component.titleWidth, height: 100.0))) if component.bottomHeight > 0.0 { excludeRects.append(CGRect(origin: CGPoint(x: 0.0, y: component.defaultHeight - component.bottomHeight), size: CGSize(width: availableSize.width, height: component.bottomHeight))) } let positionGenerator = PositionGenerator( containerSize: CGSize(width: availableSize.width, height: component.defaultHeight), - centerFrame: CGSize(width: 100, height: 100).centered(around: avatarCenter), + centerFrame: component.avatarSize.centered(around: avatarCenter), exclusionZones: excludeRects, minimumDistance: 42.0, edgePadding: 5.0, @@ -298,26 +304,28 @@ public final class PeerInfoGiftsCoverComponent: Component { } iconLayer.glowing = component.hasBackground - let centerPosition = component.avatarCenter - let finalPosition = iconPosition.center.offsetBy(dx: component.avatarCenter.x, dy: component.avatarCenter.y) - let itemScaleFraction = patternScaleValueAt(fraction: component.avatarTransitionFraction, t: 0.0, reverse: false) + let itemDistanceFraction = max(0.0, min(0.5, (iconPosition.distance - component.avatarSize.width / 2.0) / 144.0)) + let itemScaleFraction = patternScaleValueAt(fraction: min(1.0, component.avatarTransitionFraction * 1.33), t: itemDistanceFraction, reverse: false) - func interpolateRect(from: CGPoint, to: CGPoint, t: CGFloat) -> CGPoint { + func interpolatePosition(from: PositionGenerator.Position, to: PositionGenerator.Position, t: CGFloat) -> PositionGenerator.Position { let clampedT = max(0, min(1, t)) - let interpolatedX = from.x + (to.x - from.x) * clampedT - let interpolatedY = from.y + (to.y - from.y) * clampedT + let interpolatedDistance = from.distance + (to.distance - from.distance) * clampedT + let interpolatedAngle = from.angle + (to.angle - from.angle) * clampedT - return CGPoint( - x: interpolatedX, - y: interpolatedY - ) + return PositionGenerator.Position(distance: interpolatedDistance, angle: interpolatedAngle, scale: from.scale) } - let effectivePosition = interpolateRect(from: finalPosition, to: centerPosition, t: itemScaleFraction) + let toAngle: CGFloat = .pi * 0.18 + let centerPosition = PositionGenerator.Position(distance: 0.0, angle: iconPosition.angle + toAngle, scale: iconPosition.scale) + let effectivePosition = interpolatePosition(from: iconPosition, to: centerPosition, t: itemScaleFraction) + let effectiveAngle = toAngle * itemScaleFraction + let absolutePosition = getAbsolutePosition(position: effectivePosition, centerPoint: component.avatarCenter) + iconTransition.setBounds(layer: iconLayer, bounds: CGRect(origin: .zero, size: iconSize)) - iconTransition.setPosition(layer: iconLayer, position: effectivePosition) + iconTransition.setPosition(layer: iconLayer, position: absolutePosition) + iconLayer.updateRotation(effectiveAngle, transition: iconTransition) iconTransition.setScale(layer: iconLayer, scale: iconPosition.scale * (1.0 - itemScaleFraction)) iconTransition.setAlpha(layer: iconLayer, alpha: 1.0 - itemScaleFraction) @@ -580,7 +588,12 @@ private class GiftIconLayer: SimpleLayer { override func layoutSublayers() { self.shadowLayer.frame = CGRect(origin: .zero, size: self.bounds.size).insetBy(dx: -8.0, dy: -8.0) - self.animationLayer.frame = CGRect(origin: .zero, size: self.bounds.size) + self.animationLayer.bounds = CGRect(origin: .zero, size: self.bounds.size) + self.animationLayer.position = CGPoint(x: self.bounds.width / 2.0, y: self.bounds.height / 2.0) + } + + func updateRotation(_ angle: CGFloat, transition: ComponentTransition) { + self.animationLayer.transform = CATransform3DMakeRotation(angle, 0.0, 0.0, 1.0) } func startAnimations(index: Int) { @@ -638,8 +651,16 @@ private class GiftIconLayer: SimpleLayer { private struct PositionGenerator { struct Position { - let center: CGPoint + let distance: CGFloat + let angle: CGFloat let scale: CGFloat + + var relativeCartesian: CGPoint { + return CGPoint( + x: self.distance * cos(self.angle), + y: self.distance * sin(self.angle) + ) + } } let containerSize: CGSize @@ -700,15 +721,13 @@ private struct PositionGenerator { let orbitRangeSize = self.innerOrbitRange.max - self.innerOrbitRange.min let orbitDistanceFactor = self.innerOrbitRange.min + orbitRangeSize * CGFloat(self.lokiRng.next()) - let orbitDistance = orbitDistanceFactor * centerRadius + let distance = orbitDistanceFactor * centerRadius let angleRange: CGFloat = placeOnLeftSide ? .pi : .pi let angleOffset: CGFloat = placeOnLeftSide ? .pi/2 : -(.pi/2) let angle = angleOffset + angleRange * CGFloat(self.lokiRng.next()) - let absoluteX = centerPoint.x + orbitDistance * cos(angle) - let absoluteY = centerPoint.y + orbitDistance * sin(angle) - let absolutePosition = CGPoint(x: absoluteX, y: absoluteY) + let absolutePosition = getAbsolutePosition(distance: distance, angle: angle, centerPoint: centerPoint) if absolutePosition.x - itemSize.width/2 < self.edgePadding || absolutePosition.x + itemSize.width/2 > self.containerSize.width - self.edgePadding || @@ -717,11 +736,6 @@ private struct PositionGenerator { continue } - let relativePosition = CGPoint( - x: absolutePosition.x - centerPoint.x, - y: absolutePosition.y - centerPoint.y - ) - let itemRect = CGRect( x: absolutePosition.x - itemSize.width/2, y: absolutePosition.y - itemSize.height/2, @@ -729,10 +743,12 @@ private struct PositionGenerator { height: itemSize.height ) - if self.isValidPosition(itemRect, existingPositions: positions.map { self.posToAbsolute($0.center, centerPoint: centerPoint) }, itemSize: itemSize) { + if self.isValidPosition(itemRect, existingPositions: positions.map { + getAbsolutePosition(distance: $0.distance, angle: $0.angle, centerPoint: centerPoint) + }, itemSize: itemSize) { let scaleRangeSize = max(self.scaleRange.min + 0.1, 0.75) - self.scaleRange.max let scale = self.scaleRange.max + scaleRangeSize * CGFloat(self.lokiRng.next()) - positions.append(Position(center: relativePosition, scale: scale)) + positions.append(Position(distance: distance, angle: angle, scale: scale)) if absolutePosition.x < centerPoint.x { leftPositions += 1 @@ -751,16 +767,13 @@ private struct PositionGenerator { let orbitRangeSize = self.outerOrbitRange.max - self.outerOrbitRange.min let orbitDistanceFactor = self.outerOrbitRange.min + orbitRangeSize * CGFloat(self.lokiRng.next()) - let orbitDistance = orbitDistanceFactor * centerRadius + let distance = orbitDistanceFactor * centerRadius let angleRange: CGFloat = placeOnLeftSide ? .pi : .pi let angleOffset: CGFloat = placeOnLeftSide ? .pi/2 : -(.pi/2) let angle = angleOffset + angleRange * CGFloat(self.lokiRng.next()) - let absoluteX = centerPoint.x + orbitDistance * cos(angle) - let absoluteY = centerPoint.y + orbitDistance * sin(angle) - let absolutePosition = CGPoint(x: absoluteX, y: absoluteY) - + let absolutePosition = getAbsolutePosition(distance: distance, angle: angle, centerPoint: centerPoint) if absolutePosition.x - itemSize.width/2 < self.edgePadding || absolutePosition.x + itemSize.width/2 > self.containerSize.width - self.edgePadding || absolutePosition.y - itemSize.height/2 < self.edgePadding || @@ -768,11 +781,6 @@ private struct PositionGenerator { continue } - let relativePosition = CGPoint( - x: absolutePosition.x - centerPoint.x, - y: absolutePosition.y - centerPoint.y - ) - let itemRect = CGRect( x: absolutePosition.x - itemSize.width/2, y: absolutePosition.y - itemSize.height/2, @@ -780,12 +788,12 @@ private struct PositionGenerator { height: itemSize.height ) - if self.isValidPosition(itemRect, existingPositions: positions.map { self.posToAbsolute($0.center, centerPoint: centerPoint) }, itemSize: itemSize) { - let distance = hypot(absolutePosition.x - centerPoint.x, absolutePosition.y - centerPoint.y) - + if self.isValidPosition(itemRect, existingPositions: positions.map { + getAbsolutePosition(distance: $0.distance, angle: $0.angle, centerPoint: centerPoint) + }, itemSize: itemSize) { let normalizedDistance = min(distance / maxPossibleDistance, 1.0) let scale = self.scaleRange.max - normalizedDistance * (self.scaleRange.max - self.scaleRange.min) - positions.append(Position(center: relativePosition, scale: scale)) + positions.append(Position(distance: distance, angle: angle, scale: scale)) if absolutePosition.x < centerPoint.x { leftPositions += 1 @@ -798,8 +806,11 @@ private struct PositionGenerator { return positions } - private func posToAbsolute(_ relativePos: CGPoint, centerPoint: CGPoint) -> CGPoint { - return CGPoint(x: relativePos.x + centerPoint.x, y: relativePos.y + centerPoint.y) + func getAbsolutePosition(distance: CGFloat, angle: CGFloat, centerPoint: CGPoint) -> CGPoint { + return CGPoint( + x: centerPoint.x + distance * cos(angle), + y: centerPoint.y + distance * sin(angle) + ) } private func isValidPosition(_ rect: CGRect, existingPositions: [CGPoint], itemSize: CGSize) -> Bool { @@ -827,6 +838,20 @@ private struct PositionGenerator { } } +private func getAbsolutePosition(position: PositionGenerator.Position, centerPoint: CGPoint) -> CGPoint { + return CGPoint( + x: centerPoint.x + position.distance * cos(position.angle), + y: centerPoint.y + position.distance * sin(position.angle) + ) +} + +private func getAbsolutePosition(distance: CGFloat, angle: CGFloat, centerPoint: CGPoint) -> CGPoint { + return CGPoint( + x: centerPoint.x + distance * cos(angle), + y: centerPoint.y + distance * sin(angle) + ) +} + private func windowFunction(t: CGFloat) -> CGFloat { return bezierPoint(0.6, 0.0, 0.4, 1.0, t) } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift index 787b697927..4816daf1b6 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift @@ -188,6 +188,7 @@ public final class LoadingOverlayNode: ASDisplayNode { }, openWebApp: { _ in }, openPhotoSetup: { }, openAdInfo: { _ in + }, openAccountFreezeInfo: { }) let items = (0 ..< 1).map { _ -> ChatListItem in @@ -548,6 +549,8 @@ private final class PeerInfoScreenPersonalChannelItemNode: PeerInfoScreenItemNod openPhotoSetup: { }, openAdInfo: { _ in + }, + openAccountFreezeInfo: { } ) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButtonContainerNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButtonContainerNode.swift index 3b08ca63bd..98202da6c3 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButtonContainerNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButtonContainerNode.swift @@ -74,6 +74,7 @@ final class PeerInfoHeaderNavigationButtonContainerNode: SparseNode { func update(size: CGSize, presentationData: PresentationData, leftButtons: [PeerInfoHeaderNavigationButtonSpec], rightButtons: [PeerInfoHeaderNavigationButtonSpec], expandFraction: CGFloat, shouldAnimateIn: Bool, transition: ContainedViewLayoutTransition) { let sideInset: CGFloat = 24.0 + let expandedSideInset: CGFloat = 16.0 let maximumExpandOffset: CGFloat = 14.0 let expandOffset: CGFloat = -expandFraction * maximumExpandOffset @@ -188,7 +189,7 @@ final class PeerInfoHeaderNavigationButtonContainerNode: SparseNode { self.currentRightButtons = rightButtons var nextRegularButtonOrigin = size.width - sideInset - 8.0 - var nextExpandedButtonOrigin = size.width - sideInset - 8.0 + var nextExpandedButtonOrigin = size.width - expandedSideInset for spec in rightButtons.reversed() { let buttonNode: PeerInfoHeaderNavigationButton var wasAdded = false @@ -257,7 +258,7 @@ final class PeerInfoHeaderNavigationButtonContainerNode: SparseNode { for key in removeKeys { if let buttonNode = self.rightButtonNodes.removeValue(forKey: key) { if key == .moreSearchSort || key == .searchWithTags || key == .standaloneSearch { - buttonNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak buttonNode] _ in + buttonNode.layer.animateAlpha(from: buttonNode.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak buttonNode] _ in buttonNode?.removeFromSupernode() }) buttonNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false) @@ -268,7 +269,7 @@ final class PeerInfoHeaderNavigationButtonContainerNode: SparseNode { } } else { var nextRegularButtonOrigin = size.width - sideInset - 8.0 - var nextExpandedButtonOrigin = size.width - sideInset - 8.0 + var nextExpandedButtonOrigin = size.width - expandedSideInset for spec in rightButtons.reversed() { var key = spec.key diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift index 1c0c8c45df..0033cb9aa8 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift @@ -2289,6 +2289,9 @@ final class PeerInfoHeaderNode: ASDisplayNode { } if !buttonKeys.isEmpty { backgroundDefaultHeight = 327.0 + if metrics.isTablet { + backgroundDefaultHeight += 60.0 + } } hasBackground = true } else if let peer { @@ -2308,6 +2311,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { files: [:], isDark: presentationData.theme.overallDarkAppearance, avatarCenter: apparentAvatarFrame.center.offsetBy(dx: bannerInset, dy: 0.0), + avatarSize: apparentAvatarFrame.size, avatarScale: avatarScale, defaultHeight: backgroundDefaultHeight, gradientCenter: CGPoint(x: 0.5, y: buttonKeys.isEmpty ? 0.5 : 0.45), @@ -2352,6 +2356,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { giftsContext: profileGiftsContext, hasBackground: hasBackground, avatarCenter: apparentAvatarFrame.center, + avatarSize: apparentAvatarFrame.size, defaultHeight: backgroundDefaultHeight, avatarTransitionFraction: max(0.0, min(1.0, titleCollapseFraction + transitionFraction * 2.0)), statusBarHeight: statusBarHeight, diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift index e924b19dd6..66026f44ab 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift @@ -379,7 +379,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr let optionSpacing: CGFloat = 10.0 let itemsSideInset = params.sideInset + 16.0 - let defaultItemsInRow = params.size.width > params.size.height ? 5 : 3 + let defaultItemsInRow = params.size.width > params.size.height || params.size.width > 414.0 ? 5 : 3 let itemsInRow = max(1, min(starsProducts.count, defaultItemsInRow)) let defaultOptionWidth = (params.size.width - itemsSideInset * 2.0 - optionSpacing * CGFloat(defaultItemsInRow - 1)) / CGFloat(defaultItemsInRow) let optionWidth = (params.size.width - itemsSideInset * 2.0 - optionSpacing * CGFloat(itemsInRow - 1)) / CGFloat(itemsInRow) @@ -613,7 +613,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr } var bottomScrollInset: CGFloat = 0.0 - var contentHeight = ceil(CGFloat(starsProducts.count) / 3.0) * (starsOptionSize.height + optionSpacing) - optionSpacing + topInset + 16.0 + var contentHeight = ceil(CGFloat(starsProducts.count) / CGFloat(defaultItemsInRow)) * (starsOptionSize.height + optionSpacing) - optionSpacing + topInset + 16.0 let size = params.size let sideInset = params.sideInset @@ -677,13 +677,14 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr let buttonSideInset = sideInset + 16.0 let buttonSize = CGSize(width: size.width - buttonSideInset * 2.0, height: 50.0) - var bottomPanelHeight = max(8.0, bottomInset) + buttonSize.height + 8.0 + let effectiveBottomInset = max(8.0, bottomInset) + var bottomPanelHeight = effectiveBottomInset + buttonSize.height + 8.0 if params.visibleHeight < 110.0 { scrollOffset -= bottomPanelHeight } let panelTransition = ComponentTransition.immediate - panelTransition.setFrame(view: panelButton.view, frame: CGRect(origin: CGPoint(x: buttonSideInset, y: size.height - bottomInset - buttonSize.height - scrollOffset), size: buttonSize)) + panelTransition.setFrame(view: panelButton.view, frame: CGRect(origin: CGPoint(x: buttonSideInset, y: size.height - effectiveBottomInset - buttonSize.height - scrollOffset), size: buttonSize)) panelTransition.setAlpha(view: panelButton.view, alpha: panelAlpha) let _ = panelButton.updateLayout(width: buttonSize.width, transition: .immediate) @@ -754,7 +755,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr if panelCheckView.superview == nil { self.view.addSubview(panelCheckView) } - panelCheckView.frame = CGRect(origin: CGPoint(x: floor((size.width - panelCheckSize.width) / 2.0), y: size.height - bottomInset - panelCheckSize.height - 11.0 - scrollOffset), size: panelCheckSize) + panelCheckView.frame = CGRect(origin: CGPoint(x: floor((size.width - panelCheckSize.width) / 2.0), y: size.height - effectiveBottomInset - panelCheckSize.height - 11.0 - scrollOffset), size: panelCheckSize) panelTransition.setAlpha(view: panelCheckView, alpha: panelAlpha) } panelButton.isHidden = true @@ -998,7 +999,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr }))) } - if canReorder { + if case .unique = gift.gift, canReorder { items.append(.action(ContextMenuActionItem(text: strings.PeerInfo_Gifts_Context_Reorder, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, f in c?.dismiss(completion: { [weak self] in guard let self else { @@ -1015,7 +1016,26 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr guard let self else { return } - let _ = self.context.engine.accountData.setStarGiftStatus(starGift: uniqueGift, expirationDate: nil).startStandalone() + if self.context.isPremium { + let _ = self.context.engine.accountData.setStarGiftStatus(starGift: uniqueGift, expirationDate: nil).startStandalone() + } else { + let text = strings.Gift_View_TooltipPremiumWearing + let tooltipController = UndoOverlayController( + presentationData: presentationData, + content: .premiumPaywall(title: nil, text: text, customUndoText: nil, timeout: nil, linkAction: nil), + position: .bottom, + animateInAsReplacement: false, + appearance: UndoOverlayController.Appearance(sideInset: 16.0, bottomInset: 62.0), + action: { [weak self] action in + if let self, case .info = action { + let premiumController = self.context.sharedContext.makePremiumIntroController(context: self.context, source: .messageEffects, forceDark: false, dismissed: nil) + self.parentController?.push(premiumController) + } + return false + } + ) + self.parentController?.present(tooltipController, in: .current) + } }) }))) } diff --git a/submodules/TelegramUI/Components/Premium/PremiumCoinComponent/BUILD b/submodules/TelegramUI/Components/Premium/PremiumCoinComponent/BUILD new file mode 100644 index 0000000000..faac7b084f --- /dev/null +++ b/submodules/TelegramUI/Components/Premium/PremiumCoinComponent/BUILD @@ -0,0 +1,27 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "PremiumCoinComponent", + module_name = "PremiumCoinComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/ComponentFlow", + "//submodules/AccountContext", + "//submodules/AppBundle", + "//submodules/GZip", + "//submodules/LegacyComponents", + "//submodules/Components/MultilineTextComponent:MultilineTextComponent", + "//submodules/TelegramUI/Components/Premium/PremiumStarComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/PremiumUI/Sources/PremiumCoinComponent.swift b/submodules/TelegramUI/Components/Premium/PremiumCoinComponent/Sources/PremiumCoinComponent.swift similarity index 99% rename from submodules/PremiumUI/Sources/PremiumCoinComponent.swift rename to submodules/TelegramUI/Components/Premium/PremiumCoinComponent/Sources/PremiumCoinComponent.swift index 3a4a434cb6..866f24c1c5 100644 --- a/submodules/PremiumUI/Sources/PremiumCoinComponent.swift +++ b/submodules/TelegramUI/Components/Premium/PremiumCoinComponent/Sources/PremiumCoinComponent.swift @@ -48,6 +48,9 @@ public final class PremiumCoinComponent: Component { public final class View: UIView, SCNSceneRendererDelegate, ComponentTaggedView { public final class Tag { + public init() { + + } } public func matches(tag: Any) -> Bool { @@ -58,7 +61,7 @@ public final class PremiumCoinComponent: Component { } private var _ready = Promise() - var ready: Signal { + public var ready: Signal { return self._ready.get() } diff --git a/submodules/TelegramUI/Components/Settings/AccountFreezeInfoScreen/BUILD b/submodules/TelegramUI/Components/Settings/AccountFreezeInfoScreen/BUILD new file mode 100644 index 0000000000..0f7c19aaa1 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/AccountFreezeInfoScreen/BUILD @@ -0,0 +1,36 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "AccountFreezeInfoScreen", + module_name = "AccountFreezeInfoScreen", + 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/AccountContext", + "//submodules/AppBundle", + "//submodules/Components/SheetComponent", + "//submodules/PresentationDataUtils", + "//submodules/Components/BundleIconComponent", + "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/Components/BalancedTextComponent", + "//submodules/TelegramUI/Components/LottieComponent", + "//submodules/Markdown", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Settings/AccountFreezeInfoScreen/Sources/AccountFreezeInfoScreen.swift b/submodules/TelegramUI/Components/Settings/AccountFreezeInfoScreen/Sources/AccountFreezeInfoScreen.swift new file mode 100644 index 0000000000..9de68c6b39 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/AccountFreezeInfoScreen/Sources/AccountFreezeInfoScreen.swift @@ -0,0 +1,578 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import SwiftSignalKit +import TelegramCore +import Markdown +import TextFormat +import TelegramPresentationData +import TelegramStringFormatting +import ViewControllerComponent +import SheetComponent +import BundleIconComponent +import BalancedTextComponent +import MultilineTextComponent +import LottieComponent +import ButtonComponent +import AccountContext + +private final class SheetContent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let configuration: AccountFreezeConfiguration + let submitAppeal: () -> Void + let dismiss: () -> Void + + init( + context: AccountContext, + configuration: AccountFreezeConfiguration, + submitAppeal: @escaping () -> Void, + dismiss: @escaping () -> Void + ) { + self.context = context + self.configuration = configuration + self.submitAppeal = submitAppeal + self.dismiss = dismiss + } + + static func ==(lhs: SheetContent, rhs: SheetContent) -> Bool { + if lhs.context !== rhs.context { + return false + } + return true + } + + final class State: ComponentState { + var cachedChevronImage: (UIImage, PresentationTheme)? + 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 animation = Child(LottieComponent.self) + + let title = Child(BalancedTextComponent.self) + let list = Child(List.self) + let actionButton = Child(ButtonComponent.self) + let closeButton = Child(ButtonComponent.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 textColor = theme.actionSheet.primaryTextColor + let secondaryTextColor = theme.actionSheet.secondaryTextColor + let linkColor = theme.actionSheet.controlAccentColor + + let spacing: CGFloat = 16.0 + var contentSize = CGSize(width: context.availableSize.width, height: 32.0) + + let animationHeight: CGFloat = 120.0 + let animation = animation.update( + component: LottieComponent( + content: LottieComponent.AppBundleContent(name: "Banned"), + startingPosition: .begin, + playOnce: state.playOnce + ), + environment: {}, + availableSize: CGSize(width: animationHeight, height: animationHeight), + transition: .immediate + ) + context.add(animation + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + animation.size.height / 2.0)) + ) + contentSize.height += animation.size.height + contentSize.height += spacing + 5.0 + + let title = title.update( + component: BalancedTextComponent( + text: .plain(NSAttributedString(string: "Your Account is Frozen", 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 - 2.0 + + //TODO:localize + var items: [AnyComponentWithIdentity] = [] + items.append( + AnyComponentWithIdentity( + id: "ads", + component: AnyComponent(ParagraphComponent( + title: "Violation of Terms", + titleColor: textColor, + text: "Your account was frozen for breaking Telegram's Terms and Conditions.", + textColor: secondaryTextColor, + iconName: "Account Freeze/Violation", + iconColor: linkColor + )) + ) + ) + items.append( + AnyComponentWithIdentity( + id: "split", + component: AnyComponent(ParagraphComponent( + title: "Read-Only Mode", + titleColor: textColor, + text: "You can access your account but can't send messages or take actions.", + textColor: secondaryTextColor, + iconName: "Ads/Privacy", + iconColor: linkColor + )) + ) + ) + let dateString = stringForFullDate(timestamp: component.configuration.freezeUntilDate ?? 0, strings: strings, dateTimeFormat: environment.dateTimeFormat) + items.append( + AnyComponentWithIdentity( + id: "withdrawal", + component: AnyComponent(ParagraphComponent( + title: "Appeal Before Deactivation", + titleColor: textColor, + text: "Appeal via [@SpamBot]() before \(dateString), or your account will be deleted.", + textColor: secondaryTextColor, + iconName: "Account Freeze/Appeal", + iconColor: linkColor, + action: { + component.dismiss() + Queue.mainQueue().after(0.5) { + component.submitAppeal() + } + } + )) + ) + ) + + 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 + 2.0 + + let buttonAttributedString = NSMutableAttributedString(string: "Submit an Appeal", font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) + let actionButton = actionButton.update( + component: ButtonComponent( + background: ButtonComponent.Background( + color: environment.theme.list.itemCheckColors.fillColor, + foreground: environment.theme.list.itemCheckColors.foregroundColor, + pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9), + cornerRadius: 10.0 + ), + content: AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString))) + ), + isEnabled: true, + displaysProgress: false, + action: { + component.dismiss() + Queue.mainQueue().after(0.5) { + component.submitAppeal() + } + } + ), + 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)) + .cornerRadius(10.0) + ) + contentSize.height += actionButton.size.height + contentSize.height += 8.0 + + let closeAttributedString = NSMutableAttributedString(string: "Understood", font: Font.regular(17.0), textColor: environment.theme.list.itemCheckColors.fillColor, paragraphAlignment: .center) + let closeButton = closeButton.update( + component: ButtonComponent( + background: ButtonComponent.Background( + color: .clear, + foreground: .clear, + pressedColor: .clear, + cornerRadius: 10.0 + ), + content: AnyComponentWithIdentity( + id: AnyHashable(1), + component: AnyComponent(MultilineTextComponent(text: .plain(closeAttributedString))) + ), + isEnabled: true, + displaysProgress: false, + action: { + component.dismiss() + } + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), + transition: context.transition + ) + context.add(closeButton + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + actionButton.size.height / 2.0)) + ) + contentSize.height += closeButton.size.height + + if environment.safeInsets.bottom > 0 { + contentSize.height += environment.safeInsets.bottom + 5.0 + } else { + contentSize.height += 12.0 + } + + state.playAnimationIfNeeded() + + return contentSize + } + } +} + +private final class SheetContainerComponent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let configuration: AccountFreezeConfiguration + let submitAppeal: () -> Void + + init( + context: AccountContext, + configuration: AccountFreezeConfiguration, + submitAppeal: @escaping () -> Void + ) { + self.context = context + self.configuration = configuration + self.submitAppeal = submitAppeal + } + + 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, + configuration: context.component.configuration, + submitAppeal: context.component.submitAppeal, + 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 + } + } +} + + +public final class AccountFreezeInfoScreen: ViewControllerComponentContainer { + private let context: AccountContext + + public init( + context: AccountContext + ) { + self.context = context + + let configuration = AccountFreezeConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) + var submitAppealImpl: (() -> Void)? + super.init( + context: context, + component: SheetContainerComponent( + context: context, + configuration: configuration, + submitAppeal: { + submitAppealImpl?() + } + ), + navigationBarAppearance: .none, + statusBarStyle: .ignore, + theme: .default + ) + + self.navigationPresentation = .flatModal + + submitAppealImpl = { [weak self] in + guard let self, let url = configuration.freezeAppealUrl else { + return + } + context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: url, forceExternal: false, presentationData: context.sharedContext.currentPresentationData.with { $0 }, navigationController: self.navigationController as? NavigationController, dismissInput: {}) + } + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func viewDidLoad() { + super.viewDidLoad() + + self.view.disablesInteractiveModalDismiss = true + } + + public 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 + let action: (() -> Void)? + + public init( + title: String, + titleColor: UIColor, + text: String, + textColor: UIColor, + iconName: String, + iconColor: UIColor, + action: (() -> Void)? = nil + ) { + self.title = title + self.titleColor = titleColor + self.text = text + self.textColor = textColor + self.iconName = iconName + self.iconColor = iconColor + self.action = action + } + + 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 linkColor = component.iconColor + let markdownAttributes = MarkdownAttributes( + body: MarkdownAttributeSet(font: textFont, textColor: textColor), + bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), + link: MarkdownAttributeSet(font: textFont, textColor: linkColor), + linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + } + ) + + let text = text.update( + component: MultilineTextComponent( + text: .markdown(text: component.text, attributes: markdownAttributes), + horizontalAlignment: .natural, + maximumNumberOfLines: 0, + lineSpacing: 0.2, + highlightColor: linkColor.withAlphaComponent(0.1), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + }, + tapAction: { attributes, _ in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { + component.action?() + } + } + ), + 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 + 18.0) + } + } +} + +private func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? { + return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.setFillColor(backgroundColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + + context.setLineWidth(2.0) + context.setLineCap(.round) + context.setStrokeColor(foregroundColor.cgColor) + + context.move(to: CGPoint(x: 10.0, y: 10.0)) + context.addLine(to: CGPoint(x: 20.0, y: 20.0)) + context.strokePath() + + context.move(to: CGPoint(x: 20.0, y: 10.0)) + context.addLine(to: CGPoint(x: 10.0, y: 20.0)) + context.strokePath() + }) +} diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift index 8c463011d9..7e0a7745b6 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift @@ -213,6 +213,8 @@ final class GreetingMessageListItemComponent: Component { openPhotoSetup: { }, openAdInfo: { _ in + }, + openAccountFreezeInfo: { } ) self.chatListNodeInteraction = chatListNodeInteraction diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift index 277046ce3f..dd19b5efa6 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift @@ -234,6 +234,8 @@ final class QuickReplySetupScreenComponent: Component { openPhotoSetup: { }, openAdInfo: { _ in + }, + openAccountFreezeInfo: { } ) diff --git a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift index 0a35bc76f5..2b419b6569 100644 --- a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift +++ b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift @@ -876,6 +876,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, ASScrollViewDelegate }, openWebApp: { _ in }, openPhotoSetup: { }, openAdInfo: { _ in + }, openAccountFreezeInfo: { }) let chatListPresentationData = ChatListPresentationData(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true) diff --git a/submodules/TelegramUI/Components/Stars/StarsBalanceOverlayComponent/BUILD b/submodules/TelegramUI/Components/Stars/StarsBalanceOverlayComponent/BUILD index 7e5550cac0..006c97027a 100644 --- a/submodules/TelegramUI/Components/Stars/StarsBalanceOverlayComponent/BUILD +++ b/submodules/TelegramUI/Components/Stars/StarsBalanceOverlayComponent/BUILD @@ -13,6 +13,7 @@ swift_library( "//submodules/AsyncDisplayKit", "//submodules/Display", "//submodules/TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit", "//submodules/AccountContext", "//submodules/ComponentFlow", "//submodules/Components/MultilineTextComponent", diff --git a/submodules/TelegramUI/Components/Stars/StarsBalanceOverlayComponent/Sources/StarsBalanceOverlayComponent.swift b/submodules/TelegramUI/Components/Stars/StarsBalanceOverlayComponent/Sources/StarsBalanceOverlayComponent.swift index dec416f208..c304e8ca32 100644 --- a/submodules/TelegramUI/Components/Stars/StarsBalanceOverlayComponent/Sources/StarsBalanceOverlayComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsBalanceOverlayComponent/Sources/StarsBalanceOverlayComponent.swift @@ -2,6 +2,7 @@ import Foundation import UIKit import Display import ComponentFlow +import SwiftSignalKit import TelegramCore import AccountContext import TelegramPresentationData @@ -38,6 +39,10 @@ public final class StarsBalanceOverlayComponent: Component { private let action = ComponentView() private var component: StarsBalanceOverlayComponent? + private var state: EmptyComponentState? + + private var balance: Int64 = 0 + private var balanceDisposable: Disposable? private var cachedChevronImage: (UIImage, PresentationTheme)? @@ -53,17 +58,43 @@ public final class StarsBalanceOverlayComponent: Component { fatalError("init(coder:) has not been implemented") } + deinit { + self.balanceDisposable?.dispose() + } + @objc private func tapped() { if let component = self.component { component.action() } } + private var isUpdating = false func update(component: StarsBalanceOverlayComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } self.component = component + if self.balanceDisposable == nil, let starsContext = component.context.starsContext { + self.balanceDisposable = (starsContext.state + |> map { state -> Int64 in + return state?.balance.value ?? 0 + } + |> distinctUntilChanged + |> deliverOnMainQueue).start(next: { [weak self] balance in + guard let self else { + return + } + self.balance = balance + if !self.isUpdating { + self.state?.updated() + } + }) + } + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - let balance = presentationStringsFormattedNumber(Int32(component.context.starsContext?.currentState?.balance.value ?? 0), presentationData.dateTimeFormat.groupingSeparator) + let balance = presentationStringsFormattedNumber(Int32(self.balance), presentationData.dateTimeFormat.groupingSeparator) let attributedText = parseMarkdownIntoAttributedString( presentationData.strings.StarsBalance_YourBalance("**⭐️\(balance)**").string, @@ -121,23 +152,27 @@ public final class StarsBalanceOverlayComponent: Component { if let textView = self.text.view { if textView.superview == nil { - self.addSubview(textView) + self.backgroundView.addSubview(textView) } textView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: 10.0), size: textSize) } if let actionView = self.action.view { if actionView.superview == nil { - self.addSubview(actionView) + self.backgroundView.addSubview(actionView) } actionView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - actionSize.width) / 2.0), y: 29.0), size: actionSize) } self.backgroundView.updateColor(color: component.theme.rootController.navigationBar.opaqueBackgroundColor, transition: .immediate) self.backgroundView.update(size: size, cornerRadius: size.height / 2.0, transition: .immediate) - transition.setFrame(view: self.backgroundView, frame: CGRect(origin: .zero, size: size)) + transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - size.width) / 2.0), y: 0.0), size: size)) - return size + return CGSize(width: availableSize.width, height: size.height) + } + + public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + return self.backgroundView.frame.contains(point) } } diff --git a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift index cf2e01e40e..96ebd273a3 100644 --- a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift @@ -241,6 +241,8 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { textString = strings.Stars_Purchase_StarGiftInfo(component.peers.first?.value.compactDisplayTitle ?? "").string case .upgradeStarGift: textString = strings.Stars_Purchase_UpgradeStarGiftInfo + case .transferStarGift: + textString = strings.Stars_Purchase_TransferStarGiftInfo case let .sendMessage(peerId, _): if peerId.namespace == Namespaces.Peer.CloudUser { textString = strings.Stars_Purchase_SendMessageInfo(component.peers.first?.value.compactDisplayTitle ?? "").string @@ -828,7 +830,7 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { titleText = strings.Stars_Purchase_GetStars case .gift: titleText = strings.Stars_Purchase_GiftStars - case let .topUp(requiredStars, _), let .transfer(_, requiredStars), let .reactions(_, requiredStars), let .subscription(_, requiredStars, _), let .unlockMedia(requiredStars), let .starGift(_, requiredStars), let .upgradeStarGift(requiredStars), let .sendMessage(_, requiredStars): + case let .topUp(requiredStars, _), let .transfer(_, requiredStars), let .reactions(_, requiredStars), let .subscription(_, requiredStars, _), let .unlockMedia(requiredStars), let .starGift(_, requiredStars), let .upgradeStarGift(requiredStars), let .transferStarGift(requiredStars), let .sendMessage(_, requiredStars): titleText = strings.Stars_Purchase_StarsNeeded(Int32(requiredStars)) } @@ -1274,6 +1276,8 @@ private extension StarsPurchasePurpose { return requiredStars case let .upgradeStarGift(requiredStars): return requiredStars + case let .transferStarGift(requiredStars): + return requiredStars case let .sendMessage(_, requiredStars): return requiredStars default: diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift index b4066cab07..96093e6a99 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift @@ -1530,15 +1530,15 @@ private final class StarsTransactionSheetContent: CombinedComponent { .position(CGPoint(x: buttonFrame.midX, y: buttonFrame.midY)) ) originY += button.size.height + originY += 7.0 } context.add(closeButton .position(CGPoint(x: context.availableSize.width - environment.safeInsets.left - closeButton.size.width, y: 28.0)) ) - let contentSize = CGSize(width: context.availableSize.width, height: originY + 5.0 + environment.safeInsets.bottom) - - return contentSize + let effectiveBottomInset: CGFloat = environment.metrics.isTablet ? 0.0 : environment.safeInsets.bottom + return CGSize(width: context.availableSize.width, height: originY + 5.0 + effectiveBottomInset) } } } diff --git a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift index 46cbbfaf0a..d556d7a17c 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift @@ -589,10 +589,15 @@ private final class SheetContent: CombinedComponent { options: state?.options ?? [], purpose: purpose, completion: { [weak starsContext] stars in - starsContext?.add(balance: StarsAmount(value: stars, nanos: 0)) - Queue.mainQueue().after(0.1) { - completion() + guard let starsContext else { + return } + starsContext.add(balance: StarsAmount(value: stars, nanos: 0)) + + let _ = (starsContext.onUpdate + |> deliverOnMainQueue).start(next: { + completion() + }) } ) controller?.push(purchaseController) diff --git a/submodules/TelegramUI/Images.xcassets/Account Freeze/Appeal.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Account Freeze/Appeal.imageset/Contents.json new file mode 100644 index 0000000000..ecd534d108 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Account Freeze/Appeal.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "sandtimer_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Account Freeze/Appeal.imageset/sandtimer_30.pdf b/submodules/TelegramUI/Images.xcassets/Account Freeze/Appeal.imageset/sandtimer_30.pdf new file mode 100644 index 0000000000..740b891fa0 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Account Freeze/Appeal.imageset/sandtimer_30.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Account Freeze/Contents.json b/submodules/TelegramUI/Images.xcassets/Account Freeze/Contents.json new file mode 100644 index 0000000000..6e965652df --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Account Freeze/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Account Freeze/Violation.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Account Freeze/Violation.imageset/Contents.json new file mode 100644 index 0000000000..93a1282c31 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Account Freeze/Violation.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "hand_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Account Freeze/Violation.imageset/hand_30.pdf b/submodules/TelegramUI/Images.xcassets/Account Freeze/Violation.imageset/hand_30.pdf new file mode 100644 index 0000000000..27b21b63e4 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Account Freeze/Violation.imageset/hand_30.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/StarsCount.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Message/StarsCount.imageset/Contents.json new file mode 100644 index 0000000000..fa834c36d7 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Message/StarsCount.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "msgstar.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/StarsCount.imageset/msgstar.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Message/StarsCount.imageset/msgstar.pdf new file mode 100644 index 0000000000..5565014e2c Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Message/StarsCount.imageset/msgstar.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Components/AdMock.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Components/AdMock.imageset/Contents.json new file mode 100644 index 0000000000..cae3ed8f8e --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Components/AdMock.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "admock.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Components/AdMock.imageset/admock.png b/submodules/TelegramUI/Images.xcassets/Components/AdMock.imageset/admock.png new file mode 100644 index 0000000000..39dae925bd Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Components/AdMock.imageset/admock.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Components/PayMock.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Components/PayMock.imageset/Contents.json new file mode 100644 index 0000000000..0e82572e81 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Components/PayMock.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "MockSMS.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Components/PayMock.imageset/MockSMS.png b/submodules/TelegramUI/Images.xcassets/Components/PayMock.imageset/MockSMS.png new file mode 100644 index 0000000000..cfb9545fe2 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Components/PayMock.imageset/MockSMS.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Authorization/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Authorization/Contents.json new file mode 100644 index 0000000000..6e965652df --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Authorization/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Authorization/Cost.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Authorization/Cost.imageset/Contents.json new file mode 100644 index 0000000000..9d73b22267 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Authorization/Cost.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "highprice_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Authorization/Cost.imageset/highprice_30.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Authorization/Cost.imageset/highprice_30.pdf new file mode 100644 index 0000000000..68a0449411 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/Authorization/Cost.imageset/highprice_30.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Authorization/Support.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Authorization/Support.imageset/Contents.json new file mode 100644 index 0000000000..7c17dd312b --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Authorization/Support.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "support_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Authorization/Support.imageset/support_30.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Authorization/Support.imageset/support_30.pdf new file mode 100644 index 0000000000..c0b5077df3 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/Authorization/Support.imageset/support_30.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Authorization/Verification.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Authorization/Verification.imageset/Contents.json new file mode 100644 index 0000000000..d9c134477a --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Authorization/Verification.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "verificationcode_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Authorization/Verification.imageset/verificationcode_30.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Authorization/Verification.imageset/verificationcode_30.pdf new file mode 100644 index 0000000000..70e1d19c8c Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/Authorization/Verification.imageset/verificationcode_30.pdf differ diff --git a/submodules/TelegramUI/Sources/AccountContext.swift b/submodules/TelegramUI/Sources/AccountContext.swift index 2b14a05883..943fa81e14 100644 --- a/submodules/TelegramUI/Sources/AccountContext.swift +++ b/submodules/TelegramUI/Sources/AccountContext.swift @@ -266,6 +266,9 @@ public final class AccountContextImpl: AccountContext { public private(set) var isPremium: Bool + private var isFrozenDisposable: Disposable? + public private(set) var isFrozen: Bool + public let imageCache: AnyObject? public init(sharedContext: SharedAccountContextImpl, account: Account, limitsConfiguration: LimitsConfiguration, contentSettings: ContentSettings, appConfiguration: AppConfiguration, availableReplyColors: EngineAvailableColorOptions, availableProfileColors: EngineAvailableColorOptions, temp: Bool = false) @@ -280,6 +283,7 @@ public final class AccountContextImpl: AccountContext { self.peerNameColors = PeerNameColors.with(availableReplyColors: availableReplyColors, availableProfileColors: availableProfileColors) self.audioTranscriptionTrial = AudioTranscription.TrialState.defaultValue self.isPremium = false + self.isFrozen = false self.downloadedMediaStoreManager = DownloadedMediaStoreManagerImpl(postbox: account.postbox, accountManager: sharedContext.accountManager) @@ -294,7 +298,7 @@ public final class AccountContextImpl: AccountContext { self.wallpaperUploadManager = WallpaperUploadManagerImpl(sharedContext: sharedContext, account: account, presentationData: sharedContext.presentationData) self.themeUpdateManager = ThemeUpdateManagerImpl(sharedContext: sharedContext, account: account) - self.inAppPurchaseManager = InAppPurchaseManager(engine: self.engine) + self.inAppPurchaseManager = InAppPurchaseManager(engine: .authorized(self.engine)) self.starsContext = self.engine.payments.peerStarsContext() } else { self.prefetchManager = nil @@ -354,10 +358,11 @@ public final class AccountContextImpl: AccountContext { let _ = currentAppConfiguration.swap(value) }) + let langCode = sharedContext.currentPresentationData.with { $0 }.strings.baseLanguageCode self.currentCountriesConfiguration = Atomic(value: CountriesConfiguration(countries: loadCountryCodes())) if !temp { let currentCountriesConfiguration = self.currentCountriesConfiguration - self.countriesConfigurationDisposable = (self.engine.localization.getCountriesList(accountManager: sharedContext.accountManager, langCode: nil) + self.countriesConfigurationDisposable = (self.engine.localization.getCountriesList(accountManager: sharedContext.accountManager, langCode: langCode) |> deliverOnMainQueue).start(next: { value in let _ = currentCountriesConfiguration.swap(CountriesConfiguration(countries: value)) }) @@ -451,6 +456,18 @@ public final class AccountContextImpl: AccountContext { } self.audioTranscriptionTrial = audioTranscriptionTrial }) + + self.isFrozenDisposable = (self.appConfiguration + |> map { appConfiguration in + return AccountFreezeConfiguration.with(appConfiguration: appConfiguration).freezeUntilDate != nil + } + |> distinctUntilChanged + |> deliverOnMainQueue).startStrict(next: { [weak self] isFrozen in + guard let self = self else { + return + } + self.isFrozen = isFrozen + }) } deinit { @@ -463,6 +480,7 @@ public final class AccountContextImpl: AccountContext { self.animatedEmojiStickersDisposable?.dispose() self.userLimitsConfigurationDisposable?.dispose() self.peerNameColorsConfigurationDisposable?.dispose() + self.isFrozenDisposable?.dispose() } public func storeSecureIdPassword(password: String) { diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerFrozenAccount.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerFrozenAccount.swift new file mode 100644 index 0000000000..c4d0348db4 --- /dev/null +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerFrozenAccount.swift @@ -0,0 +1,32 @@ +import Foundation +import UIKit +import SwiftSignalKit +import Postbox +import TelegramCore +import AsyncDisplayKit +import Display +import ContextUI +import UndoUI +import AccountContext +import ChatControllerInteraction +import AnimatedTextComponent +import ChatMessagePaymentAlertController +import TelegramPresentationData +import TelegramNotices + +extension ChatControllerImpl { + func presentAccountFrozenInfoIfNeeded() -> Bool { + if self.context.isFrozen { + let accountFreezeConfiguration = AccountFreezeConfiguration.with(appConfiguration: self.context.currentAppConfiguration.with { $0 }) + if let freezeAppealUrl = accountFreezeConfiguration.freezeAppealUrl { + let components = freezeAppealUrl.components(separatedBy: "/") + if let username = components.last, let peer = self.presentationInterfaceState.renderedPeer?.peer, peer.addressName == username { + return false + } + } + self.push(self.context.sharedContext.makeAccountFreezeInfoScreen(context: self.context)) + return true + } + return false + } +} diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift index 3913193b89..b38259a3c5 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift @@ -1865,6 +1865,12 @@ extension ChatControllerImpl { guard let strongSelf = self, strongSelf.isNodeLoaded else { return } + + guard !strongSelf.presentAccountFrozenInfoIfNeeded() else { + completion(.immediate, {}) + return + } + if let messageId = messageId { let intrinsicCanSendMessagesHere = canSendMessagesToChat(strongSelf.presentationInterfaceState) var canSendMessagesHere = intrinsicCanSendMessagesHere @@ -2114,6 +2120,11 @@ extension ChatControllerImpl { }) }, deleteMessages: { [weak self] messages, contextController, completion in if let strongSelf = self, !messages.isEmpty { + guard !strongSelf.presentAccountFrozenInfoIfNeeded() else { + completion(.default) + return + } + let messageIds = Set(messages.map { $0.id }) strongSelf.messageContextDisposable.set((strongSelf.context.sharedContext.chatAvailableMessageActions(engine: strongSelf.context.engine, accountPeerId: strongSelf.context.account.peerId, messageIds: messageIds, keepUpdated: false) |> deliverOnMainQueue).startStrict(next: { actions in @@ -4412,7 +4423,15 @@ extension ChatControllerImpl { }, openMessagePayment: { }, openBoostToUnrestrict: { [weak self] in - guard let self, let peerId = self.chatLocation.peerId, let cachedData = self.peerView?.cachedData as? CachedChannelData, let boostToUnrestrict = cachedData.boostsToUnrestrict else { + guard let self else { + return + } + + guard !self.presentAccountFrozenInfoIfNeeded() else { + return + } + + guard let peerId = self.chatLocation.peerId, let cachedData = self.peerView?.cachedData as? CachedChannelData, let boostToUnrestrict = cachedData.boostsToUnrestrict else { return } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift index 66b8679ef6..785024337b 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift @@ -333,6 +333,9 @@ extension ChatControllerImpl { } controller?.dismissWithoutContent() + guard !self.presentAccountFrozenInfoIfNeeded() else { + return + } self.presentTagPremiumPaywall() } @@ -341,6 +344,11 @@ extension ChatControllerImpl { return } + guard !self.presentAccountFrozenInfoIfNeeded() else { + controller?.dismiss(completion: {}) + return + } + guard let message = messages.first else { return } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift index 3477609073..583e2babda 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift @@ -13,6 +13,7 @@ import PresentationDataUtils import UndoUI import UrlHandling import TelegramPresentationData +import ChatInterfaceState func openWebAppImpl( context: AccountContext, @@ -182,7 +183,7 @@ func openWebAppImpl( var isInline = false var botId = botPeer.id var botName = botName - var botAddress = "" + var botAddress = botPeer.addressName ?? "" var botVerified = botPeer.isVerified if case let .inline(bot) = source { isInline = true @@ -367,8 +368,20 @@ public extension ChatControllerImpl { } } } + let inputString = "@\(botAddress) \(query)" if let chatController { - chatController.controllerInteraction?.activateSwitchInline(selectedPeer?.id ?? peerId, "@\(botAddress) \(query)", nil) + chatController.controllerInteraction?.activateSwitchInline(selectedPeer?.id ?? peerId, inputString, nil) + } else if let selectedPeer, let navigationController = context.sharedContext.mainWindow?.viewController as? NavigationController { + let textInputState = ChatTextInputState(inputText: NSAttributedString(string: inputString)) + let _ = (ChatInterfaceState.update(engine: context.engine, peerId: selectedPeer.id, threadId: nil, { currentState in + return currentState.withUpdatedComposeInputState(textInputState) + }) + |> deliverOnMainQueue).startStandalone(completed: { [weak navigationController] in + guard let navigationController else { + return + } + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(selectedPeer), subject: nil, updateTextInputState: textInputState, peekData: nil)) + }) } } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerPaidMessage.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerPaidMessage.swift index cae3d5b3d9..16d06cfde1 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerPaidMessage.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerPaidMessage.swift @@ -72,7 +72,10 @@ extension ChatControllerImpl { return } let controller = self.context.sharedContext.makeStarsPurchaseScreen(context: self.context, starsContext: starsContext, options: options, purpose: .sendMessage(peerId: peer.id, requiredStars: totalAmount), completion: { _ in - completion(false) + let _ = (starsContext.onUpdate + |> deliverOnMainQueue).start(next: { + completion(false) + }) }) self.push(controller) }) diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index f61918c4e7..31f5b3877a 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -3399,42 +3399,51 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } }, scheduleCurrentMessage: { [weak self] params in - if let strongSelf = self { - strongSelf.presentScheduleTimePicker(completion: { [weak self] time in - if let strongSelf = self { - if let _ = strongSelf.presentationInterfaceState.interfaceState.mediaDraftState { - strongSelf.sendMediaRecording(scheduleTime: time, messageEffect: (params?.effect).flatMap { - return ChatSendMessageEffect(id: $0.id) - }) - } else { - let silentPosting = strongSelf.presentationInterfaceState.interfaceState.silentPosting - strongSelf.chatDisplayNode.sendCurrentMessage(silentPosting: silentPosting, scheduleTime: time, messageEffect: (params?.effect).flatMap { - return ChatSendMessageEffect(id: $0.id) - }) { [weak self] in - if let strongSelf = self { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, saveInterfaceState: strongSelf.presentationInterfaceState.subject != .scheduledMessages, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil).withUpdatedForwardMessageIds(nil).withUpdatedForwardOptionsState(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))) } - }) - - if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp { - strongSelf.openScheduledMessages() - } + guard let self else { + return + } + guard !self.presentAccountFrozenInfoIfNeeded() else { + return + } + self.presentScheduleTimePicker(completion: { [weak self] time in + if let strongSelf = self { + if let _ = strongSelf.presentationInterfaceState.interfaceState.mediaDraftState { + strongSelf.sendMediaRecording(scheduleTime: time, messageEffect: (params?.effect).flatMap { + return ChatSendMessageEffect(id: $0.id) + }) + } else { + let silentPosting = strongSelf.presentationInterfaceState.interfaceState.silentPosting + strongSelf.chatDisplayNode.sendCurrentMessage(silentPosting: silentPosting, scheduleTime: time, messageEffect: (params?.effect).flatMap { + return ChatSendMessageEffect(id: $0.id) + }) { [weak self] in + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, saveInterfaceState: strongSelf.presentationInterfaceState.subject != .scheduledMessages, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil).withUpdatedForwardMessageIds(nil).withUpdatedForwardOptionsState(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))) } + }) + + if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp { + strongSelf.openScheduledMessages() } } } } - }) - } - }, sendScheduledMessagesNow: { [weak self] messageIds in - if let strongSelf = self { - if let _ = strongSelf.presentationInterfaceState.slowmodeState { - if let rect = strongSelf.chatDisplayNode.frameForInputActionButton() { - strongSelf.interfaceInteraction?.displaySlowmodeTooltip(strongSelf.chatDisplayNode.view, rect) - } - return - } else { - let _ = strongSelf.context.engine.messages.sendScheduledMessageNowInteractively(messageId: messageIds.first!).startStandalone() } + }) + }, sendScheduledMessagesNow: { [weak self] messageIds in + guard let self else { + return + } + guard !self.presentAccountFrozenInfoIfNeeded() else { + return + } + + if let _ = self.presentationInterfaceState.slowmodeState { + if let rect = self.chatDisplayNode.frameForInputActionButton() { + self.interfaceInteraction?.displaySlowmodeTooltip(self.chatDisplayNode.view, rect) + } + return + } else { + let _ = self.context.engine.messages.sendScheduledMessageNowInteractively(messageId: messageIds.first!).startStandalone() } }, editScheduledMessagesTime: { [weak self] messageIds in if let strongSelf = self, let messageId = messageIds.first { @@ -5786,7 +5795,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if case let .known(value) = cachedData.businessIntro { businessIntro = value } - sendPaidMessageStars = cachedData.sendPaidMessageStars + if case let .peer(peerId) = chatLocation, peerId.namespace == Namespaces.Peer.SecretChat { + } else { + sendPaidMessageStars = cachedData.sendPaidMessageStars + } } else if let cachedData = peerView.cachedData as? CachedGroupData { var invitedBy: Peer? if let invitedByPeerId = cachedData.invitedBy { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift index 57468824f2..f69d9ad7a9 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift @@ -21,6 +21,27 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState return (nil, nil) } + if context.isFrozen { + var isActuallyFrozen = true + let accountFreezeConfiguration = AccountFreezeConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) + if let freezeAppealUrl = accountFreezeConfiguration.freezeAppealUrl { + let components = freezeAppealUrl.components(separatedBy: "/") + if let username = components.last, let peer = chatPresentationInterfaceState.renderedPeer?.peer, peer.addressName == username { + isActuallyFrozen = false + } + } + if isActuallyFrozen { + if let currentPanel = (currentPanel as? ChatRestrictedInputPanelNode) ?? (currentSecondaryPanel as? ChatRestrictedInputPanelNode) { + return (currentPanel, nil) + } else { + let panel = ChatRestrictedInputPanelNode() + panel.context = context + panel.interfaceInteraction = interfaceInteraction + return (panel, nil) + } + } + } + if let _ = chatPresentationInterfaceState.search { var selectionPanel: ChatMessageSelectionInputPanelNode? if let selectionState = chatPresentationInterfaceState.interfaceState.selectionState { diff --git a/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift index 7eac909522..adedeb9680 100644 --- a/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift @@ -3,6 +3,7 @@ import UIKit import Display import AsyncDisplayKit import Postbox +import SwiftSignalKit import TelegramCore import TelegramPresentationData import LocalizedPeerData @@ -14,6 +15,7 @@ import TextNodeWithEntities import AnimationCache import MultiAnimationRenderer import AccountContext +import PremiumUI private enum ChatReportPeerTitleButton: Equatable { case block @@ -344,7 +346,7 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode { private let context: AccountContext private let animationCache: AnimationCache private let animationRenderer: MultiAnimationRenderer - + private let separatorNode: ASDisplayNode private let closeButton: HighlightableButtonNode @@ -354,12 +356,17 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode { private let emojiSeparatorNode: ASDisplayNode private var theme: PresentationTheme? + private var presentationInterfaceState: ChatPresentationInterfaceState? private var inviteInfoNode: ChatInfoTitlePanelInviteInfoNode? private var peerNearbyInfoNode: ChatInfoTitlePanelPeerNearbyInfoNode? private var cachedChevronImage: (UIImage, PresentationTheme)? + private var emojiStatusPackDisposable = MetaDisposable() + private var emojiStatusFileId: Int64? + private var emojiStatusFileAndPackTitle = Promise<(TelegramMediaFile, LoadedStickerPack)?>() + private var tapGestureRecognizer: UITapGestureRecognizer? init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer) { @@ -391,6 +398,10 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode { self.addSubnode(self.closeButton) } + deinit { + self.emojiStatusPackDisposable.dispose() + } + override func didLoad() { super.didLoad() @@ -405,27 +416,33 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode { } private func openPremiumEmojiStatusDemo() { - guard let navigationController = self.interfaceInteraction?.getNavigationController() else { + guard let navigationController = self.interfaceInteraction?.getNavigationController(), let peerId = self.presentationInterfaceState?.chatLocation.peerId, let emojiStatus = self.presentationInterfaceState?.renderedPeer?.peer?.emojiStatus, case let .emoji(fileId) = emojiStatus.content else { return } - if self.context.isPremium { - let controller = context.sharedContext.makePremiumIntroController(context: self.context, source: .animatedEmoji, forceDark: false, dismissed: nil) - navigationController.pushViewController(controller) - } else { - var replaceImpl: ((ViewController) -> Void)? - let controller = self.context.sharedContext.makePremiumDemoController(context: self.context, subject: .emojiStatus, forceDark: false, action: { [weak self] in - guard let self else { - return - } - let controller = context.sharedContext.makePremiumIntroController(context: self.context, source: .animatedEmoji, forceDark: false, dismissed: nil) - replaceImpl?(controller) - }, dismissed: nil) - replaceImpl = { [weak controller] c in - controller?.replace(with: c) + let source: Signal = self.emojiStatusFileAndPackTitle.get() + |> take(1) + |> mapToSignal { emojiStatusFileAndPack -> Signal in + if let (file, pack) = emojiStatusFileAndPack { + return .single(.emojiStatus(peerId, fileId, file, pack)) + } else { + return .complete() } - navigationController.pushViewController(controller) } + + let _ = (source + |> deliverOnMainQueue).startStandalone(next: { [weak self, weak navigationController] source in + guard let self, let navigationController else { + return + } + let controller = PremiumIntroScreen(context: self.context, source: source) + if let textView = self.emojiStatusTextNode?.view { + controller.sourceView = textView + controller.sourceRect = CGRect(origin: .zero, size: CGSize(width: textView.frame.height, height: textView.frame.height)) + } + controller.containerView = navigationController.view + navigationController.pushViewController(controller) + }) } override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> LayoutResult { @@ -436,7 +453,8 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode { self.separatorNode.backgroundColor = interfaceState.theme.rootController.navigationBar.separatorColor self.emojiSeparatorNode.backgroundColor = interfaceState.theme.rootController.navigationBar.separatorColor } - + self.presentationInterfaceState = interfaceState + var panelHeight: CGFloat = 40.0 let contentRightInset: CGFloat = 14.0 + rightInset @@ -583,11 +601,43 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode { } } - /*#if DEBUG - emojiStatus = PeerEmojiStatus(fileId: 5062172592505356289, expirationDate: nil) - #endif*/ - - if let emojiStatus = emojiStatus { + if let emojiStatus = emojiStatus, case let .emoji(fileId) = emojiStatus.content { + if self.emojiStatusFileId != fileId { + self.emojiStatusFileId = fileId + + let emojiFileAndPack = self.context.engine.stickers.resolveInlineStickers(fileIds: [fileId]) + |> mapToSignal { result in + if let emojiFile = result.first?.value { + for attribute in emojiFile.attributes { + if case let .CustomEmoji(_, _, _, packReference) = attribute, let packReference = packReference { + return self.context.engine.stickers.loadedStickerPack(reference: packReference, forceActualized: false) + |> filter { result in + if case .result = result { + return true + } else { + return false + } + } + |> mapToSignal { result -> Signal<(TelegramMediaFile, LoadedStickerPack)?, NoError> in + if case let .result(_, items, _) = result { + return .single(items.first.flatMap { ($0.file._parse(), result) }) + } else { + return .complete() + } + } + } + } + } + return .complete() + } + self.emojiStatusPackDisposable.set(emojiFileAndPack.startStrict(next: { [weak self] fileAndPackTitle in + guard let self else { + return + } + self.emojiStatusFileAndPackTitle.set(.single(fileAndPackTitle)) + })) + } + self.emojiSeparatorNode.isHidden = false transition.updateFrame(node: self.emojiSeparatorNode, frame: CGRect(origin: CGPoint(x: leftInset + 12.0, y: 40.0), size: CGSize(width: width - leftInset - rightInset - 24.0, height: UIScreenPixel))) diff --git a/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift index 0e529f49c8..2f3fe48f6a 100644 --- a/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift @@ -9,10 +9,12 @@ import TelegramStringFormatting import ChatPresentationInterfaceState import TelegramPresentationData import ChatInputPanelNode +import AccountContext final class ChatRestrictedInputPanelNode: ChatInputPanelNode { private let buttonNode: HighlightTrackingButtonNode private let textNode: ImmediateTextNode + private let subtitleNode: ImmediateTextNode private var iconView: UIImageView? private var presentationInterfaceState: ChatPresentationInterfaceState? @@ -22,12 +24,17 @@ final class ChatRestrictedInputPanelNode: ChatInputPanelNode { self.textNode.maximumNumberOfLines = 2 self.textNode.textAlignment = .center + self.subtitleNode = ImmediateTextNode() + self.subtitleNode.maximumNumberOfLines = 1 + self.subtitleNode.textAlignment = .center + self.buttonNode = HighlightTrackingButtonNode() self.buttonNode.isUserInteractionEnabled = false super.init() self.addSubnode(self.textNode) + self.addSubnode(self.subtitleNode) self.addSubnode(self.buttonNode) self.buttonNode.highligthedChanged = { [weak self] highlighted in @@ -37,11 +44,15 @@ final class ChatRestrictedInputPanelNode: ChatInputPanelNode { self.iconView?.alpha = 0.4 self.textNode.layer.removeAnimation(forKey: "opacity") self.textNode.alpha = 0.4 + self.subtitleNode.layer.removeAnimation(forKey: "opacity") + self.subtitleNode.alpha = 0.4 } else { self.iconView?.alpha = 1.0 self.iconView?.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) self.textNode.alpha = 1.0 self.textNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + self.subtitleNode.alpha = 1.0 + self.subtitleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) } } } @@ -74,7 +85,16 @@ final class ChatRestrictedInputPanelNode: ChatInputPanelNode { var iconSpacing: CGFloat = 4.0 var isUserInteractionEnabled = false - if case let .replyThread(message) = interfaceState.chatLocation, message.peerId == self.context?.account.peerId { + var accountFreezeConfiguration: AccountFreezeConfiguration? + if let context = self.context { + accountFreezeConfiguration = AccountFreezeConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) + } + //TODO:localize + if let _ = accountFreezeConfiguration?.freezeUntilDate { + self.textNode.attributedText = NSAttributedString(string: "You account is frozen", font: Font.semibold(15.0), textColor: interfaceState.theme.list.itemDestructiveColor) + self.subtitleNode.attributedText = NSAttributedString(string: "Tap to view details", font: Font.regular(13.0), textColor: interfaceState.theme.chat.inputPanel.secondaryTextColor) + isUserInteractionEnabled = true + } else if case let .replyThread(message) = interfaceState.chatLocation, message.peerId == self.context?.account.peerId { self.textNode.attributedText = NSAttributedString(string: interfaceState.strings.Chat_PanelStatusAuthorHidden, font: Font.regular(13.0), textColor: interfaceState.theme.chat.inputPanel.secondaryTextColor) } else if let threadData = interfaceState.threadData, threadData.isClosed { iconImage = PresentationResourcesChat.chatPanelLockIcon(interfaceState.theme) @@ -116,6 +136,7 @@ final class ChatRestrictedInputPanelNode: ChatInputPanelNode { let panelHeight = defaultHeight(metrics: metrics) let textSize = self.textNode.updateLayout(CGSize(width: width - leftInset - rightInset - 8.0 * 2.0, height: panelHeight)) + let subtitleSize = self.subtitleNode.updateLayout(CGSize(width: width - leftInset - rightInset - 8.0 * 2.0, height: panelHeight)) var originX: CGFloat = leftInset + floor((width - leftInset - rightInset - textSize.width) / 2.0) @@ -139,10 +160,18 @@ final class ChatRestrictedInputPanelNode: ChatInputPanelNode { iconView.removeFromSuperview() } - let textFrame = CGRect(origin: CGPoint(x: originX, y: floor((panelHeight - textSize.height) / 2.0)), size: textSize) + var combinedHeight: CGFloat = textSize.height + if subtitleSize.height > 0.0 { + combinedHeight += subtitleSize.height + 2.0 + } + let textFrame = CGRect(origin: CGPoint(x: originX, y: floor((panelHeight - combinedHeight) / 2.0)), size: textSize) self.textNode.frame = textFrame - self.buttonNode.frame = textFrame.insetBy(dx: -8.0, dy: -12.0) + let subtitleFrame = CGRect(origin: CGPoint(x: leftInset + floor((width - leftInset - rightInset - subtitleSize.width) / 2.0), y: floor((panelHeight + combinedHeight) / 2.0) - subtitleSize.height), size: subtitleSize) + self.subtitleNode.frame = subtitleFrame + + let combinedFrame = textFrame.union(subtitleFrame) + self.buttonNode.frame = combinedFrame.insetBy(dx: -8.0, dy: -12.0) return panelHeight } diff --git a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift index 915121923c..aa7b9960a4 100644 --- a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift +++ b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift @@ -296,6 +296,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, ASScrollViewDe }, openWebApp: { _ in }, openPhotoSetup: { }, openAdInfo: { _ in + }, openAccountFreezeInfo: { }) interaction.searchTextHighightState = searchQuery self.interaction = interaction diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index bf2d24cdc6..f6b6715f03 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -644,7 +644,6 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch return self.actionButtons.micButton } - private let startingBotDisposable = MetaDisposable() private let statusDisposable = MetaDisposable() override var interfaceInteraction: ChatPanelInterfaceInteraction? { didSet { @@ -655,28 +654,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch self?.updateIsProcessingInlineRequest(value) }).strict()) } - if let startingBot = self.interfaceInteraction?.statuses?.startingBot { - self.startingBotDisposable.set((startingBot |> deliverOnMainQueue).startStrict(next: { [weak self] value in - if let strongSelf = self { - strongSelf.startingBotProgress = value - } - }).strict()) - } } } - - private var startingBotProgress = false { - didSet { -// if self.startingBotProgress != oldValue { -// if self.startingBotProgress { -// self.startButton.transitionToProgress() -// } else { -// self.startButton.transitionFromProgress() -// } -// } - } - } - + func updateInputTextState(_ state: ChatTextInputState, keepSendButtonEnabled: Bool, extendedSearchLayout: Bool, accessoryItems: [ChatTextInputAccessoryItem], animated: Bool) { if let currentState = self.presentationInterfaceState { var updateAccessoryButtons = false @@ -1130,7 +1110,6 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch deinit { self.statusDisposable.dispose() - self.startingBotDisposable.dispose() self.tooltipController?.dismiss() self.currentEmojiSuggestion?.disposable.dispose() } diff --git a/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift index 855cc81ef2..7fdf358ddb 100644 --- a/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift @@ -183,6 +183,8 @@ private struct CommandChatInputContextPanelEntry: Comparable, Identifiable { openPhotoSetup: { }, openAdInfo: { _ in + }, + openAccountFreezeInfo: { } ) diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index e8b79259a2..0e2dedb349 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -77,6 +77,7 @@ import ContentReportScreen import AffiliateProgramSetupScreen import GalleryUI import ShareController +import AccountFreezeInfoScreen private final class AccountUserInterfaceInUseContext { let subscribers = Bag<(Bool) -> Void>() @@ -2390,13 +2391,11 @@ public final class SharedAccountContextImpl: SharedAccountContext { } } - public func makePremiumIntroController(context: AccountContext, source: PremiumIntroSource, forceDark: Bool, dismissed: (() -> Void)?) -> ViewController { - var modal = true + private func mapIntroSource(source: PremiumIntroSource) -> PremiumSource { let mappedSource: PremiumSource switch source { case .settings: mappedSource = .settings - modal = false case .stickers: mappedSource = .stickers case .reactions: @@ -2477,12 +2476,32 @@ public final class SharedAccountContextImpl: SharedAccountContext { mappedSource = .animatedEmoji case .paidMessages: mappedSource = .paidMessages + case let .auth(price): + mappedSource = .auth(price) } - let controller = PremiumIntroScreen(context: context, source: mappedSource, modal: modal, forceDark: forceDark) + return mappedSource + } + + public func makePremiumIntroController(context: AccountContext, source: PremiumIntroSource, forceDark: Bool, dismissed: (() -> Void)?) -> ViewController { + var modal = true + if case .settings = source { + modal = false + } + let controller = PremiumIntroScreen(context: context, source: self.mapIntroSource(source: source), modal: modal, forceDark: forceDark) controller.wasDismissed = dismissed return controller } + public func makePremiumIntroController(sharedContext: SharedAccountContext, engine: TelegramEngineUnauthorized, inAppPurchaseManager: InAppPurchaseManager, source: PremiumIntroSource, proceed: (() -> Void)?) -> ViewController { + var modal = true + if case .settings = source { + modal = false + } + let controller = PremiumIntroScreen(screenContext: .sharedContext(sharedContext, engine, inAppPurchaseManager), source: self.mapIntroSource(source: source), modal: modal) + controller.customProceed = proceed + return controller + } + public func makePremiumDemoController(context: AccountContext, subject: PremiumDemoSubject, forceDark: Bool, action: @escaping () -> Void, dismissed: (() -> Void)?) -> ViewController { let presentationData = context.sharedContext.currentPresentationData.with { $0 } var buttonText: String = presentationData.strings.Common_OK @@ -2883,62 +2902,102 @@ public final class SharedAccountContextImpl: SharedAccountContext { } } + let optionsPromise = Promise<[StarsTopUpOption]?>(nil) + if let state = context.starsContext?.currentState, state.balance < StarsAmount(value: 100, nanos: 0) { + optionsPromise.set(context.engine.payments.starsTopUpOptions() + |> map(Optional.init)) + } + presentTransferAlertImpl = { [weak controller] peer in guard let controller, case let .starGiftTransfer(_, _, gift, transferStars, _, _) = source else { return } - let alertController = giftTransferAlertController(context: context, gift: gift, peer: peer, transferStars: transferStars, commit: { [weak controller] in - completion?([peer.id]) - - guard let controller, let navigationController = controller.navigationController as? NavigationController else { - return - } - var controllers = navigationController.viewControllers - controllers = controllers.filter { !($0 is ContactSelectionController) } - - if !isChannelGift { - if peer.id.namespace == Namespaces.Peer.CloudChannel { - if let controller = context.sharedContext.makePeerInfoController( - context: context, - updatedPresentationData: nil, - peer: peer._asPeer(), - mode: .gifts, - avatarInitiallyExpanded: false, - fromChat: false, - requestsContext: nil - ) { - controllers.append(controller) + let alertController = giftTransferAlertController( + context: context, + gift: gift, + peer: peer, + transferStars: transferStars, + navigationController: controller.navigationController as? NavigationController, + commit: { [weak controller] in + let proceed: (Bool) -> Void = { waitForTopUp in + completion?([peer.id]) + + guard let controller, let navigationController = controller.navigationController as? NavigationController else { + return } - } else { - var foundController = false - for controller in controllers.reversed() { - if let chatController = controller as? ChatController, case .peer(id: peer.id) = chatController.chatLocation { - chatController.hintPlayNextOutgoingGift() - foundController = true - break + var controllers = navigationController.viewControllers + controllers = controllers.filter { !($0 is ContactSelectionController) } + + if !isChannelGift { + if peer.id.namespace == Namespaces.Peer.CloudChannel { + if let controller = context.sharedContext.makePeerInfoController( + context: context, + updatedPresentationData: nil, + peer: peer._asPeer(), + mode: .gifts, + avatarInitiallyExpanded: false, + fromChat: false, + requestsContext: nil + ) { + controllers.append(controller) + } + } else { + var foundController = false + for controller in controllers.reversed() { + if let chatController = controller as? ChatController, case .peer(id: peer.id) = chatController.chatLocation { + chatController.hintPlayNextOutgoingGift() + foundController = true + break + } + } + if !foundController { + let chatController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .standard(.default), params: nil) + chatController.hintPlayNextOutgoingGift() + controllers.append(chatController) + } } } - if !foundController { - let chatController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .standard(.default), params: nil) - chatController.hintPlayNextOutgoingGift() - controllers.append(chatController) + navigationController.setViewControllers(controllers, animated: true) + + Queue.mainQueue().after(0.3) { + let tooltipController = UndoOverlayController( + presentationData: presentationData, + content: .forward(savedMessages: false, text: presentationData.strings.Gift_Transfer_Success("\(gift.title) #\(presentationStringsFormattedNumber(gift.number, presentationData.dateTimeFormat.groupingSeparator))", peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string), + elevatedLayout: false, + action: { _ in return true } + ) + if let lastController = controllers.last as? ViewController { + lastController.present(tooltipController, in: .window(.root)) + } } } - } - navigationController.setViewControllers(controllers, animated: true) - - Queue.mainQueue().after(0.3) { - let tooltipController = UndoOverlayController( - presentationData: presentationData, - content: .forward(savedMessages: false, text: presentationData.strings.Gift_Transfer_Success("\(gift.title) #\(presentationStringsFormattedNumber(gift.number, presentationData.dateTimeFormat.groupingSeparator))", peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string), - elevatedLayout: false, - action: { _ in return true } - ) - if let lastController = controllers.last as? ViewController { - lastController.present(tooltipController, in: .window(.root)) + + if transferStars > 0, let starsContext = context.starsContext, let starsState = starsContext.currentState { + if starsState.balance < StarsAmount(value: transferStars, nanos: 0) { + let _ = (optionsPromise.get() + |> filter { $0 != nil } + |> take(1) + |> deliverOnMainQueue).startStandalone(next: { [weak controller] options in + let purchaseController = context.sharedContext.makeStarsPurchaseScreen( + context: context, + starsContext: starsContext, + options: options ?? [], + purpose: .transferStarGift(requiredStars: transferStars), + completion: { stars in + starsContext.add(balance: StarsAmount(value: stars, nanos: 0)) + proceed(true) + } + ) + controller?.push(purchaseController) + }) + } else { + proceed(false) + } + } else { + proceed(false) } } - }) + ) controller.present(alertController, in: .window(.root)) } @@ -3494,6 +3553,10 @@ public final class SharedAccountContextImpl: SharedAccountContext { } return controller } + + public func makeAccountFreezeInfoScreen(context: AccountContext) -> ViewController { + return AccountFreezeInfoScreen(context: context) + } } private func peerInfoControllerImpl(context: AccountContext, updatedPresentationData: (PresentationData, Signal)?, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, requestsContext: PeerInvitationImportersContext? = nil) -> ViewController? { diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index ccef0a2cde..21a1fe7c2b 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -2232,6 +2232,9 @@ public final class WebAppController: ViewController, AttachmentContainable { self.webView?.sendEvent(name: "fullscreen_changed", data: paramsString) controller.isFullscreen = isFullscreen + if isFullscreen { + controller.requestAttachmentMenuExpansion() + } if let (layout, _) = self.validLayout, case .regular = layout.metrics.widthClass { if let snapshotView = self.webView?.snapshotView(afterScreenUpdates: false) { diff --git a/versions.json b/versions.json index 26fd2ba0c2..9146b480e6 100644 --- a/versions.json +++ b/versions.json @@ -1,5 +1,5 @@ { - "app": "11.8.1", + "app": "11.8.2", "xcode": "16.2", "bazel": "7.3.1:981f82a470bad1349322b6f51c9c6ffa0aa291dab1014fac411543c12e661dff", "macos": "15"