From a573daa9494c0c4e83b7e3e03119678d1e69edf7 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 23 Jun 2022 18:41:21 +0500 Subject: [PATCH 1/3] Various fixes --- submodules/Display/Source/ListView.swift | 3 ++ .../ChatMessageAnimatedStickerItemNode.swift | 46 +++++++++---------- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/submodules/Display/Source/ListView.swift b/submodules/Display/Source/ListView.swift index 25a669b16c..b482bc67ad 100644 --- a/submodules/Display/Source/ListView.swift +++ b/submodules/Display/Source/ListView.swift @@ -3347,6 +3347,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture if progress == 1.0 { for itemNode in temporaryPreviousNodes { + itemNode.visibility = .none itemNode.removeFromSupernode() itemNode.extractedBackgroundNode?.removeFromSupernode() } @@ -3360,6 +3361,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture } else { animation.completion = { _ in for itemNode in temporaryPreviousNodes { + itemNode.visibility = .none itemNode.removeFromSupernode() itemNode.extractedBackgroundNode?.removeFromSupernode() } @@ -3442,6 +3444,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture private func removeItemNodeAtIndex(_ index: Int) { let node = self.itemNodes[index] self.itemNodes.remove(at: index) + node.visibility = .none node.removeFromSupernode() node.extractedBackgroundNode?.removeFromSupernode() node.accessoryItemNode?.removeFromSupernode() diff --git a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift index 8df35f3722..f6fbf62026 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -402,11 +402,11 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } } - private var visibilityStatus: Bool = false { + private var visibilityStatus: Bool? { didSet { if self.visibilityStatus != oldValue { self.updateVisibility() - self.haptic?.enabled = self.visibilityStatus + self.haptic?.enabled = self.visibilityStatus == true } } } @@ -593,28 +593,18 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } } - let isPlaying = self.visibilityStatus && !self.forceStopAnimations - if let animationNode = self.animationNode as? AnimatedStickerNode { - if !isPlaying { - for decorationNode in self.additionalAnimationNodes { - if let transitionNode = item.controllerInteraction.getMessageTransitionNode() { - decorationNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak decorationNode] _ in - if let decorationNode = decorationNode { - transitionNode.remove(decorationNode: decorationNode) - } - }) - } - } - self.additionalAnimationNodes.removeAll() - - if let overlayMeshAnimationNode = self.overlayMeshAnimationNode { - self.overlayMeshAnimationNode = nil - if let transitionNode = item.controllerInteraction.getMessageTransitionNode() { - transitionNode.remove(decorationNode: overlayMeshAnimationNode) - } + let isPlaying = self.visibilityStatus == true && !self.forceStopAnimations + if !isPlaying { + self.removeAdditionalAnimations() + + if let overlayMeshAnimationNode = self.overlayMeshAnimationNode { + self.overlayMeshAnimationNode = nil + if let transitionNode = item.controllerInteraction.getMessageTransitionNode() { + transitionNode.remove(decorationNode: overlayMeshAnimationNode) } } - + } + if let animationNode = self.animationNode as? AnimatedStickerNode { if self.isPlaying != isPlaying { self.isPlaying = isPlaying @@ -1721,10 +1711,20 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { self.additionalAnimationNodes.append(decorationNode) - additionalAnimationNode.play() + additionalAnimationNode.visibility = true } } + private func removeAdditionalAnimations() { + for decorationNode in self.additionalAnimationNodes { + if let additionalAnimationNode = decorationNode.contentView.asyncdisplaykit_node as? AnimatedStickerNode { + additionalAnimationNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak additionalAnimationNode] _ in + additionalAnimationNode?.visibility = false + }) + } + } + } + private func gestureRecognized(gesture: TapLongTapOrDoubleTapGesture, location: CGPoint, recognizer: TapLongTapOrDoubleTapGestureRecognizer?) -> InternalBubbleTapAction? { switch gesture { case .tap: From 4e21428c101a039fb3bee2c30fc58e7339a707bc Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 24 Jun 2022 20:20:27 +0500 Subject: [PATCH 2/3] Fix particles animation --- .../PremiumUI/Sources/FasterStarsView.swift | 87 ++++++++++++------- 1 file changed, 58 insertions(+), 29 deletions(-) diff --git a/submodules/PremiumUI/Sources/FasterStarsView.swift b/submodules/PremiumUI/Sources/FasterStarsView.swift index f80b621f70..a96b632a1f 100644 --- a/submodules/PremiumUI/Sources/FasterStarsView.swift +++ b/submodules/PremiumUI/Sources/FasterStarsView.swift @@ -3,6 +3,7 @@ import UIKit import SceneKit import Display import AppBundle +import LegacyComponents final class FasterStarsView: UIView, PhoneDemoDecorationView { private let sceneView: SCNView @@ -55,23 +56,37 @@ final class FasterStarsView: UIView, PhoneDemoDecorationView { } self.playing = true - let speedAnimation = CABasicAnimation(keyPath: "speedFactor") - speedAnimation.fromValue = 1.0 - speedAnimation.toValue = 1.8 + let speedAnimation = POPBasicAnimation() + speedAnimation.property = (POPAnimatableProperty.property(withName: "speedFactor", initializer: { property in + property?.readBlock = { particleSystem, values in + values?.pointee = (particleSystem as! SCNParticleSystem).speedFactor + } + property?.writeBlock = { particleSystem, values in + (particleSystem as! SCNParticleSystem).speedFactor = values!.pointee + } + property?.threshold = 0.01 + }) as! POPAnimatableProperty) + speedAnimation.fromValue = 1.0 as NSNumber + speedAnimation.toValue = 3.0 as NSNumber + speedAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear) speedAnimation.duration = 0.8 - speedAnimation.fillMode = .forwards - particles.addAnimation(speedAnimation, forKey: "speedFactor") + particles.pop_add(speedAnimation, forKey: "speedFactor") - particles.speedFactor = 3.0 - - let stretchAnimation = CABasicAnimation(keyPath: "stretchFactor") - stretchAnimation.fromValue = 0.05 - stretchAnimation.toValue = 0.3 + let stretchAnimation = POPBasicAnimation() + stretchAnimation.property = (POPAnimatableProperty.property(withName: "stretchFactor", initializer: { property in + property?.readBlock = { particleSystem, values in + values?.pointee = (particleSystem as! SCNParticleSystem).stretchFactor + } + property?.writeBlock = { particleSystem, values in + (particleSystem as! SCNParticleSystem).stretchFactor = values!.pointee + } + property?.threshold = 0.01 + }) as! POPAnimatableProperty) + stretchAnimation.fromValue = 0.05 as NSNumber + stretchAnimation.toValue = 0.3 as NSNumber + stretchAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear) stretchAnimation.duration = 0.8 - stretchAnimation.fillMode = .forwards - particles.addAnimation(stretchAnimation, forKey: "stretchFactor") - - particles.stretchFactor = 0.3 + particles.pop_add(stretchAnimation, forKey: "stretchFactor") } func resetAnimation() { @@ -79,24 +94,38 @@ final class FasterStarsView: UIView, PhoneDemoDecorationView { return } self.playing = false - - let speedAnimation = CABasicAnimation(keyPath: "speedFactor") - speedAnimation.fromValue = 3.0 - speedAnimation.toValue = 1.0 + + let speedAnimation = POPBasicAnimation() + speedAnimation.property = (POPAnimatableProperty.property(withName: "speedFactor", initializer: { property in + property?.readBlock = { particleSystem, values in + values?.pointee = (particleSystem as! SCNParticleSystem).speedFactor + } + property?.writeBlock = { particleSystem, values in + (particleSystem as! SCNParticleSystem).speedFactor = values!.pointee + } + property?.threshold = 0.01 + }) as! POPAnimatableProperty) + speedAnimation.fromValue = 3.0 as NSNumber + speedAnimation.toValue = 1.0 as NSNumber + speedAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear) speedAnimation.duration = 0.35 - speedAnimation.fillMode = .forwards - particles.addAnimation(speedAnimation, forKey: "speedFactor") + particles.pop_add(speedAnimation, forKey: "speedFactor") - particles.speedFactor = 1.0 - - let stretchAnimation = CABasicAnimation(keyPath: "stretchFactor") - stretchAnimation.fromValue = 0.3 - stretchAnimation.toValue = 0.05 + let stretchAnimation = POPBasicAnimation() + stretchAnimation.property = (POPAnimatableProperty.property(withName: "stretchFactor", initializer: { property in + property?.readBlock = { particleSystem, values in + values?.pointee = (particleSystem as! SCNParticleSystem).stretchFactor + } + property?.writeBlock = { particleSystem, values in + (particleSystem as! SCNParticleSystem).stretchFactor = values!.pointee + } + property?.threshold = 0.01 + }) as! POPAnimatableProperty) + stretchAnimation.fromValue = 0.3 as NSNumber + stretchAnimation.toValue = 0.05 as NSNumber + stretchAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear) stretchAnimation.duration = 0.35 - stretchAnimation.fillMode = .forwards - particles.addAnimation(stretchAnimation, forKey: "stretchFactor") - - particles.stretchFactor = 0.05 + particles.pop_add(stretchAnimation, forKey: "stretchFactor") } override func layoutSubviews() { From 1c07c18f154887f5ac2e3cb0b65432be936d3ff9 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 24 Jun 2022 20:22:28 +0500 Subject: [PATCH 3/3] Various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 69 +- .../AccountUtils/Sources/AccountUtils.swift | 1 + submodules/BuildConfig/BUILD | 2 - .../PublicHeaders/BuildConfig/BuildConfig.h | 1 - submodules/BuildConfig/Sources/BuildConfig.m | 4 - submodules/CheckNode/Sources/CheckNode.swift | 9 + .../Source/Components/RoundedRectangle.swift | 26 +- .../Sources/InAppPurchaseManager.swift | 69 +- .../Sources/InviteLinkHeaderItem.swift | 49 +- .../Sources/IncreaseLimitFooterItem.swift | 2 +- submodules/PremiumUI/BUILD | 2 +- submodules/PremiumUI/Resources/gift.scn | Bin 0 -> 30009 bytes .../PremiumUI/Sources/DataRainView.swift | 4 - .../Sources/GiftAvatarComponent.swift | 294 ++++ .../PremiumUI/Sources/PremiumGiftScreen.swift | 1286 +++++++++++++++++ .../Sources/PremiumIntroScreen.swift | 132 +- .../Sources/PremiumStarComponent.swift | 2 +- .../PremiumUI/Sources/ScrollComponent.swift | 132 ++ submodules/SettingsUI/BUILD | 1 + .../Sources/DeleteAccountDataController.swift | 190 +++ .../Sources/DeleteAccountFooterItem.swift | 152 ++ .../DeleteAccountOptionsController.swift | 349 +++++ .../PresentationResourcesSettings.swift | 5 + .../Context Menu/Gift.imageset/Contents.json | 12 + .../Chat/Context Menu/Gift.imageset/gift.pdf | 512 +++++++ .../Menu/ClearSynced.imageset/Contents.json | 12 + .../Menu/ClearSynced.imageset/clearsynced.pdf | 136 ++ .../DeleteAddAccount.imageset/Contents.json | 12 + .../DeleteAddAccount.imageset/addaccount.pdf | 102 ++ .../Menu/DeleteChats.imageset/Contents.json | 12 + .../Menu/DeleteChats.imageset/deletechats.pdf | 175 +++ .../TelegramUI/Sources/AccountContext.swift | 7 +- .../TelegramUI/Sources/AppDelegate.swift | 2 +- .../Sources/ChatMessageDateHeader.swift | 16 + .../Sources/NotificationContentContext.swift | 2 +- .../Sources/PeerInfo/PeerInfoScreen.swift | 192 +-- .../Sources/ShareExtensionContext.swift | 2 +- .../Sources/SharedAccountContext.swift | 6 +- 38 files changed, 3707 insertions(+), 274 deletions(-) create mode 100644 submodules/PremiumUI/Resources/gift.scn create mode 100644 submodules/PremiumUI/Sources/GiftAvatarComponent.swift create mode 100644 submodules/PremiumUI/Sources/PremiumGiftScreen.swift create mode 100644 submodules/PremiumUI/Sources/ScrollComponent.swift create mode 100644 submodules/SettingsUI/Sources/DeleteAccountDataController.swift create mode 100644 submodules/SettingsUI/Sources/DeleteAccountFooterItem.swift create mode 100644 submodules/SettingsUI/Sources/DeleteAccountOptionsController.swift create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Gift.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Gift.imageset/gift.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Settings/Menu/ClearSynced.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Settings/Menu/ClearSynced.imageset/clearsynced.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Settings/Menu/DeleteAddAccount.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Settings/Menu/DeleteAddAccount.imageset/addaccount.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Settings/Menu/DeleteChats.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Settings/Menu/DeleteChats.imageset/deletechats.pdf diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 4f2eeb4d95..5e39ed1472 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -868,7 +868,6 @@ "MessageTimer.Years_2" = "%@ years"; "MessageTimer.Years_3_10" = "%@ years"; "MessageTimer.Years_any" = "%@ years"; -"MessageTimer.Months_many" = "%@ years"; "MessageTimer.ShortSeconds_1" = "%@s"; "MessageTimer.ShortSeconds_2" = "%@s"; @@ -7725,7 +7724,6 @@ Sorry for the inconvenience."; "Premium.Restore.Success" = "Done"; "Premium.Restore.ErrorUnknown" = "An error occurred. Please try again."; - "Settings.Premium" = "Telegram Premium"; "Settings.AddAnotherAccount.PremiumHelp" = "You can add up to four accounts with different phone numbers."; @@ -7735,3 +7733,70 @@ Sorry for the inconvenience."; "Appearance.AppIconPremium" = "Premium"; "Appearance.AppIconBlack" = "Black"; "Appearance.AppIconTurbo" = "Turbo"; + +"PrivacySettings.DeleteAccountNow" = "Delete Account Now"; + +"DeleteAccount.AlternativeOptionsTitle" = "Alternative Options"; + +"DeleteAccount.Options.ChangePhoneNumberTitle" = "Change Phone Number"; +"DeleteAccount.Options.ChangePhoneNumberText" = "Move your contacts, chats and media to a new number."; + +"DeleteAccount.Options.AddAccountTitle" = "Add Another Account"; +"DeleteAccount.Options.AddAccountText" = "You can use up to 3 accounts in one app at the same time."; +"DeleteAccount.Options.AddAccountPremiumText" = "You can use up to 4 accounts in one app at the same time."; + +"DeleteAccount.Options.ChangePrivacyTitle" = "Change Your Privacy Settings"; +"DeleteAccount.Options.ChangePrivacyText" = "Choose who exactly can see which of your info."; + +"DeleteAccount.Options.SetTwoStepAuthTitle" = "Enable Two-Step Verification"; +"DeleteAccount.Options.SetTwoStepAuthText" = "Set a password that will be required each time you log in."; + +"DeleteAccount.Options.SetPasscodeTitle" = "Set a Passcode"; +"DeleteAccount.Options.SetPasscodeText" = "Lock the app with a passcode so that others can't open it."; + +"DeleteAccount.Options.ClearCacheTitle" = "Clear Cache"; +"DeleteAccount.Options.ClearCacheText" = "Free up disk space on your device; your media will stay in the cloud."; + +"DeleteAccount.Options.ClearSyncedContactsTitle" = "Clear Synced Contacts"; +"DeleteAccount.Options.ClearSyncedContactsText" = "Remove any unnecessary contacts you may have synced."; + +"DeleteAccount.Options.DeleteChatsTitle" = "Quickly Delete Your Chats"; +"DeleteAccount.Options.DeleteChatsText" = "Learn how to remove any info you don’t need in a few taps."; + +"DeleteAccount.Options.ContactSupportTitle" = "Contact Support"; +"DeleteAccount.Options.ContactSupportText" = "Tell us about any issues; deleting account doesn't usually help."; + +"DeleteAccount.DeleteMyAccountTitle" = "Delete My Account"; +"DeleteAccount.DeleteMyAccount" = "Delete My Account"; + +"DeleteAccount.ComeBackLater" = "Come Back Later"; +"DeleteAccount.Continue" = "Continue"; + +"DeleteAccount.CloudStorageTitle" = "Your Free Cloud Storage"; +"DeleteAccount.CloudStorageText" = "You will lose access to all your Saved Messages as well as all messages, media and files from your chats."; + +"DeleteAccount.GroupsAndChannelsTitle" = "Your Groups and Channels"; +"DeleteAccount.GroupsAndChannelsText" = "The groups and channels you created will either get new admins or become orphaned."; +"DeleteAccount.GroupsAndChannelsInfo" = "You can transfer group and channel ownership to other users via Chat Info > Edit > Admins. [More info]()"; + +"DeleteAccount.MessageHistoryTitle" = "Your Message History"; +"DeleteAccount.MessageHistoryText" = "Your chat partners will keep their message history with you, including the messages you shared in secret chats.\n\nYou can remove any messages for both sides at any time, but this will not be possible if you delete your account. [More info]()"; + +"DeleteAccount.ConfirmationAlertTitle" = "Proceed to Delete Your Account?"; +"DeleteAccount.ConfirmationAlertText" = "Deleting your account will permanently delete your data!\n\nIt is imposible to reverse this action!"; +"DeleteAccount.ConfirmationAlertDelete" = "Delete My Account"; + +"PeerInfo.GiftPremium" = "Gift Premium"; + +"Premium.Gift.Title" = "Gift Telegram Premium"; +"Premium.Gift.Description" = "Let **%@** enjoy exclusive features of Telegram with **Telegram Premium**."; +"Premium.Gift.Info" = "You can review the list of features and terms of use for Telegram Premium [here]()."; +"Premium.Gift.GiftSubscription" = "Gift Subscription for %@"; + +"Premium.Gift.PricePerMonth" = "%@ / month"; + +"Premium.Gift.Months_1" = "%@ Month"; +"Premium.Gift.Months_any" = "%@ Months"; + +"Premium.Gift.Years_1" = "%@ Year"; +"Premium.Gift.Years_any" = "%@ Years"; diff --git a/submodules/AccountUtils/Sources/AccountUtils.swift b/submodules/AccountUtils/Sources/AccountUtils.swift index 76d533e0f0..44d09f560f 100644 --- a/submodules/AccountUtils/Sources/AccountUtils.swift +++ b/submodules/AccountUtils/Sources/AccountUtils.swift @@ -5,6 +5,7 @@ import TelegramUIPreferences import AccountContext public let maximumNumberOfAccounts = 3 +public let maximumPremiumNumberOfAccounts = 4 public func activeAccountsAndPeers(context: AccountContext, includePrimary: Bool = false) -> Signal<((AccountContext, EnginePeer)?, [(AccountContext, EnginePeer, Int32)]), NoError> { let sharedContext = context.sharedContext diff --git a/submodules/BuildConfig/BUILD b/submodules/BuildConfig/BUILD index 83ee53988e..10c5c92dbb 100644 --- a/submodules/BuildConfig/BUILD +++ b/submodules/BuildConfig/BUILD @@ -7,7 +7,6 @@ load( "telegram_is_appstore_build", "telegram_appstore_id", "telegram_app_specific_url_scheme", - "telegram_premium_iap_product_id", ) objc_library( @@ -26,7 +25,6 @@ objc_library( "-DAPP_CONFIG_IS_APPSTORE_BUILD={}".format(telegram_is_appstore_build), "-DAPP_CONFIG_APPSTORE_ID={}".format(telegram_appstore_id), "-DAPP_SPECIFIC_URL_SCHEME=\\\"{}\\\"".format(telegram_app_specific_url_scheme), - "-DAPP_CONFIG_PREMIUM_IAP_PRODUCT_ID=\\\"{}\\\"".format(telegram_premium_iap_product_id), ], hdrs = glob([ "PublicHeaders/**/*.h", diff --git a/submodules/BuildConfig/PublicHeaders/BuildConfig/BuildConfig.h b/submodules/BuildConfig/PublicHeaders/BuildConfig/BuildConfig.h index a433870b93..d735dd74bc 100644 --- a/submodules/BuildConfig/PublicHeaders/BuildConfig/BuildConfig.h +++ b/submodules/BuildConfig/PublicHeaders/BuildConfig/BuildConfig.h @@ -18,7 +18,6 @@ @property (nonatomic, readonly) bool isAppStoreBuild; @property (nonatomic, readonly) int64_t appStoreId; @property (nonatomic, strong, readonly) NSString * _Nonnull appSpecificUrlScheme; -@property (nonatomic, strong, readonly) NSString * _Nonnull premiumIAPProductId; + (DeviceSpecificEncryptionParameters * _Nonnull)deviceSpecificEncryptionParameters:(NSString * _Nonnull)rootPath baseAppBundleId:(NSString * _Nonnull)baseAppBundleId; - (NSData * _Nullable)bundleDataWithAppToken:(NSData * _Nullable)appToken signatureDict:(NSDictionary * _Nullable)signatureDict; diff --git a/submodules/BuildConfig/Sources/BuildConfig.m b/submodules/BuildConfig/Sources/BuildConfig.m index a589f33372..3dde1749ae 100644 --- a/submodules/BuildConfig/Sources/BuildConfig.m +++ b/submodules/BuildConfig/Sources/BuildConfig.m @@ -185,10 +185,6 @@ API_AVAILABLE(ios(10)) return @(APP_SPECIFIC_URL_SCHEME); } -- (NSString *)premiumIAPProductId { - return @(APP_CONFIG_PREMIUM_IAP_PRODUCT_ID); -} - + (NSString * _Nullable)bundleSeedId { NSDictionary *query = [NSDictionary dictionaryWithObjectsAndKeys: (__bridge NSString *)kSecClassGenericPassword, (__bridge NSString *)kSecClass, diff --git a/submodules/CheckNode/Sources/CheckNode.swift b/submodules/CheckNode/Sources/CheckNode.swift index edc567d3f8..227c77c35b 100644 --- a/submodules/CheckNode/Sources/CheckNode.swift +++ b/submodules/CheckNode/Sources/CheckNode.swift @@ -312,6 +312,15 @@ public class CheckLayer: CALayer { } } + public override init() { + self.theme = CheckNodeTheme(backgroundColor: .white, strokeColor: .blue, borderColor: .white, overlayBorder: false, hasInset: false, hasShadow: false) + self.content = .check + + super.init() + + self.isOpaque = false + } + public init(theme: CheckNodeTheme, content: CheckNodeContent = .check) { self.theme = theme self.content = content diff --git a/submodules/ComponentFlow/Source/Components/RoundedRectangle.swift b/submodules/ComponentFlow/Source/Components/RoundedRectangle.swift index 3c8153a594..3d1f8a3e1f 100644 --- a/submodules/ComponentFlow/Source/Components/RoundedRectangle.swift +++ b/submodules/ComponentFlow/Source/Components/RoundedRectangle.swift @@ -10,15 +10,17 @@ public final class RoundedRectangle: Component { public let colors: [UIColor] public let cornerRadius: CGFloat public let gradientDirection: GradientDirection + public let stroke: CGFloat? - public convenience init(color: UIColor, cornerRadius: CGFloat) { - self.init(colors: [color], cornerRadius: cornerRadius) + public convenience init(color: UIColor, cornerRadius: CGFloat, stroke: CGFloat? = nil) { + self.init(colors: [color], cornerRadius: cornerRadius, stroke: stroke) } - public init(colors: [UIColor], cornerRadius: CGFloat, gradientDirection: GradientDirection = .horizontal) { + public init(colors: [UIColor], cornerRadius: CGFloat, gradientDirection: GradientDirection = .horizontal, stroke: CGFloat? = nil) { self.colors = colors self.cornerRadius = cornerRadius self.gradientDirection = gradientDirection + self.stroke = stroke } public static func ==(lhs: RoundedRectangle, rhs: RoundedRectangle) -> Bool { @@ -31,6 +33,9 @@ public final class RoundedRectangle: Component { if lhs.gradientDirection != rhs.gradientDirection { return false } + if lhs.stroke != rhs.stroke { + return false + } return true } @@ -40,11 +45,16 @@ public final class RoundedRectangle: Component { func update(component: RoundedRectangle, availableSize: CGSize, transition: Transition) -> CGSize { if self.component != component { if component.colors.count == 1, let color = component.colors.first { - let imageSize = CGSize(width: component.cornerRadius * 2.0, height: component.cornerRadius * 2.0) + let imageSize = CGSize(width: max(component.stroke ?? 0.0, component.cornerRadius) * 2.0, height: max(component.stroke ?? 0.0, component.cornerRadius) * 2.0) UIGraphicsBeginImageContextWithOptions(imageSize, false, 0.0) if let context = UIGraphicsGetCurrentContext() { context.setFillColor(color.cgColor) context.fillEllipse(in: CGRect(origin: CGPoint(), size: imageSize)) + + if let stroke = component.stroke, stroke > 0.0 { + context.setBlendMode(.clear) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: imageSize).insetBy(dx: stroke, dy: stroke)) + } } self.image = UIGraphicsGetImageFromCurrentImageContext()?.stretchableImage(withLeftCapWidth: Int(component.cornerRadius), topCapHeight: Int(component.cornerRadius)) UIGraphicsEndImageContext() @@ -66,6 +76,14 @@ public final class RoundedRectangle: Component { } let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: component.gradientDirection == .horizontal ? CGPoint(x: imageSize.width, y: 0.0) : CGPoint(x: 0.0, y: imageSize.height), options: CGGradientDrawingOptions()) + + if let stroke = component.stroke, stroke > 0.0 { + context.resetClip() + + context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: imageSize).insetBy(dx: stroke, dy: stroke), cornerRadius: component.cornerRadius).cgPath) + context.setBlendMode(.clear) + context.fill(CGRect(origin: .zero, size: imageSize)) + } } self.image = UIGraphicsGetImageFromCurrentImageContext()?.stretchableImage(withLeftCapWidth: Int(component.cornerRadius), topCapHeight: Int(component.cornerRadius)) UIGraphicsEndImageContext() diff --git a/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift b/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift index 91eb0e8231..21f7dc3d6c 100644 --- a/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift +++ b/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift @@ -5,20 +5,68 @@ import StoreKit import Postbox import TelegramCore +private let productIdentifiers = [ + "org.telegram.telegramPremium.monthly", + "org.telegram.telegramPremium.twelveMonths", + "org.telegram.telegramPremium.sixMonths", + "org.telegram.telegramPremium.threeMonths" +] + public final class InAppPurchaseManager: NSObject { - public final class Product { + public final class Product: Equatable { + private lazy var numberFormatter: NumberFormatter = { + let numberFormatter = NumberFormatter() + numberFormatter.numberStyle = .currency + numberFormatter.locale = self.skProduct.priceLocale + return numberFormatter + }() + let skProduct: SKProduct init(skProduct: SKProduct) { self.skProduct = skProduct } - public var price: String { - let numberFormatter = NumberFormatter() - numberFormatter.numberStyle = .currency - numberFormatter.locale = self.skProduct.priceLocale - return numberFormatter.string(from: self.skProduct.price) ?? "" + public var id: String { + return self.skProduct.productIdentifier } + + public var isSubscription: Bool { + if #available(iOS 12.0, *) { + return self.skProduct.subscriptionGroupIdentifier != nil + } else if #available(iOS 11.2, *) { + return self.skProduct.subscriptionPeriod != nil + } else { + return !self.id.contains(".monthly") + } + } + + public var price: String { + return self.numberFormatter.string(from: self.skProduct.price) ?? "" + } + + public func pricePerMonth(_ monthsCount: Int) -> String { + let price = self.skProduct.price.dividing(by: NSDecimalNumber(value: monthsCount)) + return self.numberFormatter.string(from: price) ?? "" + } + + public var priceValue: NSDecimalNumber { + return self.skProduct.price + } + + public static func ==(lhs: Product, rhs: Product) -> Bool { + if lhs.id != rhs.id { + return false + } + if lhs.isSubscription != rhs.isSubscription { + return false + } + if lhs.priceValue != rhs.priceValue { + return false + } + return true + } + } public enum PurchaseState { @@ -58,7 +106,6 @@ public final class InAppPurchaseManager: NSObject { } private let engine: TelegramEngine - private let premiumProductId: String private var products: [Product] = [] private var productsPromise = Promise<[Product]>([]) @@ -71,9 +118,8 @@ public final class InAppPurchaseManager: NSObject { private let disposableSet = DisposableDict() - public init(engine: TelegramEngine, premiumProductId: String) { + public init(engine: TelegramEngine) { self.engine = engine - self.premiumProductId = premiumProductId super.init() @@ -90,11 +136,8 @@ public final class InAppPurchaseManager: NSObject { } private func requestProducts() { - guard !self.premiumProductId.isEmpty else { - return - } Logger.shared.log("InAppPurchaseManager", "Requesting products") - let productRequest = SKProductsRequest(productIdentifiers: Set([self.premiumProductId])) + let productRequest = SKProductsRequest(productIdentifiers: Set(productIdentifiers)) productRequest.delegate = self productRequest.start() diff --git a/submodules/InviteLinksUI/Sources/InviteLinkHeaderItem.swift b/submodules/InviteLinksUI/Sources/InviteLinkHeaderItem.swift index 715e5fa65b..6c7a0d04db 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkHeaderItem.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkHeaderItem.swift @@ -15,14 +15,16 @@ import TextFormat public class InviteLinkHeaderItem: ListViewItem, ItemListItem { public let context: AccountContext public let theme: PresentationTheme + public let title: String? public let text: String public let animationName: String public let sectionId: ItemListSectionId public let linkAction: ((ItemListTextItemLinkAction) -> Void)? - public init(context: AccountContext, theme: PresentationTheme, text: String, animationName: String, sectionId: ItemListSectionId, linkAction: ((ItemListTextItemLinkAction) -> Void)? = nil) { + public init(context: AccountContext, theme: PresentationTheme, title: String? = nil, text: String, animationName: String, sectionId: ItemListSectionId, linkAction: ((ItemListTextItemLinkAction) -> Void)? = nil) { self.context = context self.theme = theme + self.title = title self.text = text self.animationName = animationName self.sectionId = sectionId @@ -66,10 +68,12 @@ public class InviteLinkHeaderItem: ListViewItem, ItemListItem { } } -private let titleFont = Font.regular(13.0) +private let titleFont = Font.semibold(17.0) +private let textFont = Font.regular(14.0) class InviteLinkHeaderItemNode: ListViewItemNode { private let titleNode: TextNode + private let textNode: TextNode private var animationNode: AnimatedStickerNode private var item: InviteLinkHeaderItem? @@ -80,11 +84,17 @@ class InviteLinkHeaderItemNode: ListViewItemNode { self.titleNode.contentMode = .left self.titleNode.contentsScale = UIScreen.main.scale + self.textNode = TextNode() + self.textNode.isUserInteractionEnabled = false + self.textNode.contentMode = .left + self.textNode.contentsScale = UIScreen.main.scale + self.animationNode = DefaultAnimatedStickerNodeImpl() super.init(layerBacked: false, dynamicBounce: false) self.addSubnode(self.titleNode) + self.addSubnode(self.textNode) self.addSubnode(self.animationNode) } @@ -100,18 +110,27 @@ class InviteLinkHeaderItemNode: ListViewItemNode { func asyncLayout() -> (_ item: InviteLinkHeaderItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeTextLayout = TextNode.asyncLayout(self.textNode) return { item, params, neighbors in - let leftInset: CGFloat = 32.0 + params.leftInset + let leftInset: CGFloat = 28.0 + params.leftInset let topInset: CGFloat = 124.0 + let spacing: CGFloat = 5.0 - let attributedText = parseMarkdownIntoAttributedString(item.text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: item.theme.list.freeTextColor), bold: MarkdownAttributeSet(font: titleFont, textColor: item.theme.list.freeTextColor), link: MarkdownAttributeSet(font: titleFont, textColor: item.theme.list.itemAccentColor), linkAttribute: { contents in + let attributedTitle = NSAttributedString(string: item.title ?? "", font: titleFont, textColor: item.theme.list.itemPrimaryTextColor, paragraphAlignment: .center) + + let attributedText = parseMarkdownIntoAttributedString(item.text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: item.theme.list.freeTextColor), bold: MarkdownAttributeSet(font: textFont, textColor: item.theme.list.freeTextColor), link: MarkdownAttributeSet(font: textFont, textColor: item.theme.list.itemAccentColor), linkAttribute: { contents in return (TelegramTextAttributes.URL, contents) })) - - let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) - let contentSize = CGSize(width: params.width, height: topInset + titleLayout.size.height) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: attributedTitle, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) + + let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) + + var contentSize = CGSize(width: params.width, height: topInset + textLayout.size.height) + if let _ = item.title { + contentSize.height += titleLayout.size.height + spacing + } let insets = itemListNeighborsGroupedInsets(neighbors, params) let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) @@ -129,8 +148,16 @@ class InviteLinkHeaderItemNode: ListViewItemNode { strongSelf.animationNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - iconSize.width) / 2.0), y: -10.0), size: iconSize) strongSelf.animationNode.updateLayout(size: iconSize) + var origin: CGFloat = topInset + 8.0 + let _ = titleApply() - strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - titleLayout.size.width) / 2.0), y: topInset + 8.0), size: titleLayout.size) + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - titleLayout.size.width) / 2.0), y: origin), size: titleLayout.size) + if titleLayout.size.height > 0.0 { + origin += titleLayout.size.height + spacing + } + + let _ = textApply() + strongSelf.textNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - textLayout.size.width) / 2.0), y: origin), size: textLayout.size) } }) } @@ -150,9 +177,9 @@ class InviteLinkHeaderItemNode: ListViewItemNode { if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { switch gesture { case .tap: - let titleFrame = self.titleNode.frame - if let item = self.item, titleFrame.contains(location) { - if let (_, attributes) = self.titleNode.attributesAtPoint(CGPoint(x: location.x - titleFrame.minX, y: location.y - titleFrame.minY)) { + let textFrame = self.textNode.frame + if let item = self.item, textFrame.contains(location) { + if let (_, attributes) = self.textNode.attributesAtPoint(CGPoint(x: location.x - textFrame.minX, y: location.y - textFrame.minY)) { if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { item.linkAction?(.tap(url)) } diff --git a/submodules/PeerInfoUI/Sources/IncreaseLimitFooterItem.swift b/submodules/PeerInfoUI/Sources/IncreaseLimitFooterItem.swift index 6ec922464f..befb39d4f9 100644 --- a/submodules/PeerInfoUI/Sources/IncreaseLimitFooterItem.swift +++ b/submodules/PeerInfoUI/Sources/IncreaseLimitFooterItem.swift @@ -23,7 +23,7 @@ final class IncreaseLimitFooterItem: ItemListControllerFooterItem { func isEqual(to: ItemListControllerFooterItem) -> Bool { if let item = to as? IncreaseLimitFooterItem { - return self.theme === item.theme && self.title == item.title + return self.theme === item.theme && self.title == item.title && self.colorful == item.colorful } else { return false } diff --git a/submodules/PremiumUI/BUILD b/submodules/PremiumUI/BUILD index 4728a45871..cbb0e38f16 100644 --- a/submodules/PremiumUI/BUILD +++ b/submodules/PremiumUI/BUILD @@ -82,7 +82,6 @@ swift_library( "//submodules/Components/SheetComponent:SheetComponent", "//submodules/Components/BundleIconComponent:BundleIconComponent", "//submodules/Components/SolidRoundedButtonComponent:SolidRoundedButtonComponent", - "//submodules/Components/Forms/PrefixSectionGroupComponent:PrefixSectionGroupComponent", "//submodules/InAppPurchaseManager:InAppPurchaseManager", "//submodules/ConfettiEffect:ConfettiEffect", "//submodules/TextFormat:TextFormat", @@ -93,6 +92,7 @@ swift_library( "//submodules/RadialStatusNode:RadialStatusNode", "//submodules/ShimmerEffect:ShimmerEffect", "//submodules/LegacyComponents:LegacyComponents", + "//submodules/CheckNode:CheckNode", ], visibility = [ "//visibility:public", diff --git a/submodules/PremiumUI/Resources/gift.scn b/submodules/PremiumUI/Resources/gift.scn new file mode 100644 index 0000000000000000000000000000000000000000..14f72e0681d5a22da7076ada94ca112c19c5b84f GIT binary patch literal 30009 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 B#l!#r literal 0 HcmV?d00001 diff --git a/submodules/PremiumUI/Sources/DataRainView.swift b/submodules/PremiumUI/Sources/DataRainView.swift index 122971f7d7..c3b95828b6 100644 --- a/submodules/PremiumUI/Sources/DataRainView.swift +++ b/submodules/PremiumUI/Sources/DataRainView.swift @@ -14,10 +14,6 @@ public final class MatrixView: MTKView, MTKViewDelegate, PhoneDemoDecorationView private var displayLink: CADisplayLink? -// private var metalLayer: CAMetalLayer { -// return self.layer as! CAMetalLayer -// } - private let symbolTexture: MTLTexture private let randomTexture: MTLTexture diff --git a/submodules/PremiumUI/Sources/GiftAvatarComponent.swift b/submodules/PremiumUI/Sources/GiftAvatarComponent.swift new file mode 100644 index 0000000000..9069fab68b --- /dev/null +++ b/submodules/PremiumUI/Sources/GiftAvatarComponent.swift @@ -0,0 +1,294 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import SwiftSignalKit +import SceneKit +import GZip +import AppBundle +import LegacyComponents +import AvatarNode +import AccountContext +import TelegramCore + +private let sceneVersion: Int = 3 + +private func deg2rad(_ number: Float) -> Float { + return number * .pi / 180 +} + +private func rad2deg(_ number: Float) -> Float { + return number * 180.0 / .pi +} + +private func generateParticlesTexture() -> UIImage { + return UIImage() +} + +private func generateFlecksTexture() -> UIImage { + return UIImage() +} + +private func generateShineTexture() -> UIImage { + return UIImage() +} + +private func generateDiffuseTexture() -> UIImage { + return generateImage(CGSize(width: 256, height: 256), rotatedContext: { size, context in + let colorsArray: [CGColor] = [ + UIColor(rgb: 0x0079ff).cgColor, + UIColor(rgb: 0x6a93ff).cgColor, + UIColor(rgb: 0x9172fe).cgColor, + UIColor(rgb: 0xe46acd).cgColor, + ] + var locations: [CGFloat] = [0.0, 0.25, 0.5, 0.75, 1.0] + let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray as CFArray, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: size.height), options: CGGradientDrawingOptions()) + })! +} + +class GiftAvatarComponent: Component { + let context: AccountContext + let peer: EnginePeer + let isVisible: Bool + let hasIdleAnimations: Bool + + init(context: AccountContext, peer: EnginePeer, isVisible: Bool, hasIdleAnimations: Bool) { + self.context = context + self.peer = peer + self.isVisible = isVisible + self.hasIdleAnimations = hasIdleAnimations + } + + static func ==(lhs: GiftAvatarComponent, rhs: GiftAvatarComponent) -> Bool { + return lhs.isVisible == rhs.isVisible && lhs.hasIdleAnimations == rhs.hasIdleAnimations + } + + final class View: UIView, SCNSceneRendererDelegate, ComponentTaggedView { + final class Tag { + } + + func matches(tag: Any) -> Bool { + if let _ = tag as? Tag { + return true + } + return false + } + + private var _ready = Promise() + var ready: Signal { + return self._ready.get() + } + + weak var animateFrom: UIView? + weak var containerView: UIView? + var animationColor: UIColor? + + private let sceneView: SCNView + private let avatarNode: ImageNode + + private var previousInteractionTimestamp: Double = 0.0 + private var timer: SwiftSignalKit.Timer? + private var hasIdleAnimations = false + + override init(frame: CGRect) { + self.sceneView = SCNView(frame: CGRect(origin: .zero, size: CGSize(width: 64.0, height: 64.0))) + self.sceneView.backgroundColor = .clear + self.sceneView.transform = CGAffineTransform(scaleX: 0.5, y: 0.5) + self.sceneView.isUserInteractionEnabled = false + self.sceneView.preferredFramesPerSecond = 60 + + self.avatarNode = ImageNode() + self.avatarNode.displaysAsynchronously = false + + super.init(frame: frame) + + self.addSubview(self.sceneView) + self.addSubview(self.avatarNode.view) + + self.setup() + + let panGestureRecoginzer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:))) + self.addGestureRecognizer(panGestureRecoginzer) + + let tapGestureRecoginzer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:))) + self.addGestureRecognizer(tapGestureRecoginzer) + + self.disablesInteractiveModalDismiss = true + self.disablesInteractiveTransitionGestureRecognizer = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.timer?.invalidate() + } + + private let hapticFeedback = HapticFeedback() + + private var delayTapsTill: Double? + @objc private func handleTap(_ gesture: UITapGestureRecognizer) { + self.playAppearanceAnimation(velocity: nil, mirror: false, explode: true) + } + + private var previousYaw: Float = 0.0 + @objc private func handlePan(_ gesture: UIPanGestureRecognizer) { + guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else { + return + } + + self.previousInteractionTimestamp = CACurrentMediaTime() + + if #available(iOS 11.0, *) { + node.removeAnimation(forKey: "rotate", blendOutDuration: 0.1) + node.removeAnimation(forKey: "tapRotate", blendOutDuration: 0.1) + } else { + node.removeAllAnimations() + } + + switch gesture.state { + case .began: + self.previousYaw = 0.0 + case .changed: + let translation = gesture.translation(in: gesture.view) + let yawPan = deg2rad(Float(translation.x)) + + func rubberBandingOffset(offset: CGFloat, bandingStart: CGFloat) -> CGFloat { + let bandedOffset = offset - bandingStart + let range: CGFloat = 60.0 + let coefficient: CGFloat = 0.4 + return bandingStart + (1.0 - (1.0 / ((bandedOffset * coefficient / range) + 1.0))) * range + } + + var pitchTranslation = rubberBandingOffset(offset: abs(translation.y), bandingStart: 0.0) + if translation.y < 0.0 { + pitchTranslation *= -1.0 + } + let pitchPan = deg2rad(Float(pitchTranslation)) + + self.previousYaw = yawPan + node.eulerAngles = SCNVector3(pitchPan, yawPan, 0.0) + case .ended: + let velocity = gesture.velocity(in: gesture.view) + + var smallAngle = false + if (self.previousYaw < .pi / 2 && self.previousYaw > -.pi / 2) && abs(velocity.x) < 200 { + smallAngle = true + } + + self.playAppearanceAnimation(velocity: velocity.x, smallAngle: smallAngle, explode: !smallAngle && abs(velocity.x) > 600) + node.eulerAngles = SCNVector3(0.0, 0.0, 0.0) + default: + break + } + } + + private func setup() { + guard let url = getAppBundle().url(forResource: "gift", withExtension: "scn"), let scene = try? SCNScene(url: url, options: nil) else { + return + } + + self.sceneView.scene = scene + self.sceneView.delegate = self + + let _ = self.sceneView.snapshot() + } + + private var didSetReady = false + func renderer(_ renderer: SCNSceneRenderer, didRenderScene scene: SCNScene, atTime time: TimeInterval) { + if !self.didSetReady { + self.didSetReady = true + + Queue.mainQueue().justDispatch { + self._ready.set(.single(true)) + self.onReady() + } + } + } + + private func onReady() { + self.playAppearanceAnimation(explode: true) + + self.previousInteractionTimestamp = CACurrentMediaTime() + self.timer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in + if let strongSelf = self, strongSelf.hasIdleAnimations { + let currentTimestamp = CACurrentMediaTime() + if currentTimestamp > strongSelf.previousInteractionTimestamp + 5.0 { + strongSelf.playAppearanceAnimation() + } + } + }, queue: Queue.mainQueue()) + self.timer?.start() + } + + private func playAppearanceAnimation(velocity: CGFloat? = nil, smallAngle: Bool = false, mirror: Bool = false, explode: Bool = false) { + guard let scene = self.sceneView.scene else { + return + } + + let currentTime = CACurrentMediaTime() + self.previousInteractionTimestamp = currentTime + self.delayTapsTill = currentTime + 0.85 + + if explode, let node = scene.rootNode.childNode(withName: "swirl", recursively: false), let particles = scene.rootNode.childNode(withName: "particles", recursively: false) { + if let particleSystem = particles.particleSystems?.first { + particleSystem.particleColorVariation = SCNVector4(0.15, 0.2, 0.15, 0.3) + particleSystem.speedFactor = 2.0 + particleSystem.particleVelocity = 2.2 + particleSystem.birthRate = 4.0 + particleSystem.particleLifeSpan = 2.0 + + node.physicsField?.isActive = true + Queue.mainQueue().after(1.0) { + node.physicsField?.isActive = false + particles.particleSystems?.first?.birthRate = 1.2 + particleSystem.particleVelocity = 1.0 + particleSystem.particleLifeSpan = 4.0 + + let animation = POPBasicAnimation() + animation.property = (POPAnimatableProperty.property(withName: "speedFactor", initializer: { property in + property?.readBlock = { particleSystem, values in + values?.pointee = (particleSystem as! SCNParticleSystem).speedFactor + } + property?.writeBlock = { particleSystem, values in + (particleSystem as! SCNParticleSystem).speedFactor = values!.pointee + } + property?.threshold = 0.01 + }) as! POPAnimatableProperty) + animation.fromValue = 2.0 as NSNumber + animation.toValue = 1.0 as NSNumber + animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear) + animation.duration = 0.5 + particleSystem.pop_add(animation, forKey: "speedFactor") + } + } + } + } + + func update(component: GiftAvatarComponent, availableSize: CGSize, transition: Transition) -> CGSize { + self.sceneView.bounds = CGRect(origin: .zero, size: CGSize(width: availableSize.width * 2.0, height: availableSize.height * 2.0)) + if self.sceneView.superview == self { + self.sceneView.center = CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0) + } + + self.hasIdleAnimations = component.hasIdleAnimations + + let avatarSize = CGSize(width: 100.0, height: 100.0) + self.avatarNode.setSignal(peerAvatarCompleteImage(account: component.context.account, peer: component.peer, size: avatarSize, font: avatarPlaceholderFont(size: 78.0), fullSize: true)) + self.avatarNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - avatarSize.width) / 2.0), y: 63.0), size: avatarSize) + + return availableSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, transition: transition) + } +} diff --git a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift new file mode 100644 index 0000000000..903c29b49e --- /dev/null +++ b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift @@ -0,0 +1,1286 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import SwiftSignalKit +import Postbox +import TelegramCore +import TelegramPresentationData +import PresentationDataUtils +import ViewControllerComponent +import AccountContext +import SolidRoundedButtonComponent +import MultilineTextComponent +import BundleIconComponent +import SolidRoundedButtonComponent +import Markdown +import InAppPurchaseManager +import ConfettiEffect +import TextFormat +import CheckNode + +private final class ProductGroupComponent: Component { + public final class Item: Equatable { + public let content: AnyComponentWithIdentity + public let action: () -> Void + + public init(_ content: AnyComponentWithIdentity, action: @escaping () -> Void) { + self.content = content + self.action = action + } + + public static func ==(lhs: Item, rhs: Item) -> Bool { + if lhs.content != rhs.content { + return false + } + + return true + } + } + + let items: [Item] + let backgroundColor: UIColor + let selectionColor: UIColor + + init( + items: [Item], + backgroundColor: UIColor, + selectionColor: UIColor + ) { + self.items = items + self.backgroundColor = backgroundColor + self.selectionColor = selectionColor + } + + public static func ==(lhs: ProductGroupComponent, rhs: ProductGroupComponent) -> Bool { + if lhs.items != rhs.items { + return false + } + if lhs.backgroundColor != rhs.backgroundColor { + return false + } + if lhs.selectionColor != rhs.selectionColor { + return false + } + return true + } + + public final class View: UIView { + private var buttonViews: [AnyHashable: HighlightTrackingButton] = [:] + private var itemViews: [AnyHashable: ComponentHostView] = [:] + + private var component: ProductGroupComponent? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func buttonPressed(_ sender: HighlightTrackingButton) { + guard let component = self.component else { + return + } + + if let (id, _) = self.buttonViews.first(where: { $0.value === sender }), let item = component.items.first(where: { $0.content.id == id }) { + item.action() + } + } + + func update(component: ProductGroupComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let spacing: CGFloat = 16.0 + var size = CGSize(width: availableSize.width, height: 0.0) + + var validIds: [AnyHashable] = [] + + var i = 0 + for item in component.items { + validIds.append(item.content.id) + + let buttonView: HighlightTrackingButton + let itemView: ComponentHostView + var itemTransition = transition + + if let current = self.buttonViews[item.content.id] { + buttonView = current + } else { + buttonView = HighlightTrackingButton() + buttonView.clipsToBounds = true + buttonView.layer.cornerRadius = 10.0 + if #available(iOS 13.0, *) { + buttonView.layer.cornerCurve = .continuous + } + buttonView.isMultipleTouchEnabled = false + buttonView.isExclusiveTouch = true + buttonView.addTarget(self, action: #selector(self.buttonPressed(_:)), for: .touchUpInside) + self.buttonViews[item.content.id] = buttonView + self.addSubview(buttonView) + } + buttonView.backgroundColor = component.backgroundColor + + if let current = self.itemViews[item.content.id] { + itemView = current + } else { + itemTransition = transition.withAnimation(.none) + itemView = ComponentHostView() + self.itemViews[item.content.id] = itemView + self.addSubview(itemView) + } + let itemSize = itemView.update( + transition: itemTransition, + component: item.content.component, + environment: {}, + containerSize: CGSize(width: size.width, height: .greatestFiniteMagnitude) + ) + + let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height), size: itemSize) + buttonView.frame = CGRect(origin: itemFrame.origin, size: CGSize(width: availableSize.width, height: itemSize.height + UIScreenPixel)) + itemView.frame = CGRect(origin: CGPoint(x: itemFrame.minX, y: itemFrame.minY + floor((itemFrame.height - itemSize.height) / 2.0)), size: itemSize) + itemView.isUserInteractionEnabled = false + + buttonView.highligthedChanged = { [weak buttonView] highlighted in + if highlighted { + buttonView?.backgroundColor = component.selectionColor + } else { + UIView.animate(withDuration: 0.3, animations: { + buttonView?.backgroundColor = nil + }) + } + } + + size.height += itemSize.height + spacing + + i += 1 + } + + size.height -= spacing + + var removeIds: [AnyHashable] = [] + for (id, itemView) in self.itemViews { + if !validIds.contains(id) { + removeIds.append(id) + itemView.removeFromSuperview() + } + } + for id in removeIds { + self.itemViews.removeValue(forKey: id) + } + + self.component = component + + return size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class GiftComponent: CombinedComponent { + let title: String + let totalPrice: String + let perMonthPrice: String + let discount: String + let selected: Bool + let primaryTextColor: UIColor + let secondaryTextColor: UIColor + let accentColor: UIColor + let checkForegroundColor: UIColor + let checkBorderColor: UIColor + + init( + title: String, + totalPrice: String, + perMonthPrice: String, + discount: String, + selected: Bool, + primaryTextColor: UIColor, + secondaryTextColor: UIColor, + accentColor: UIColor, + checkForegroundColor: UIColor, + checkBorderColor: UIColor + ) { + self.title = title + self.totalPrice = totalPrice + self.perMonthPrice = perMonthPrice + self.discount = discount + self.selected = selected + self.primaryTextColor = primaryTextColor + self.secondaryTextColor = secondaryTextColor + self.accentColor = accentColor + self.checkForegroundColor = checkForegroundColor + self.checkBorderColor = checkBorderColor + } + + static func ==(lhs: GiftComponent, rhs: GiftComponent) -> Bool { + if lhs.title != rhs.title { + return false + } + if lhs.totalPrice != rhs.totalPrice { + return false + } + if lhs.perMonthPrice != rhs.perMonthPrice { + return false + } + if lhs.discount != rhs.discount { + return false + } + if lhs.selected != rhs.selected { + return false + } + if lhs.primaryTextColor != rhs.primaryTextColor { + return false + } + if lhs.secondaryTextColor != rhs.secondaryTextColor { + return false + } + if lhs.accentColor != rhs.accentColor { + return false + } + if lhs.checkForegroundColor != rhs.checkForegroundColor { + return false + } + if lhs.checkBorderColor != rhs.checkBorderColor { + return false + } + return true + } + + static var body: Body { + let check = Child(CheckComponent.self) + let title = Child(MultilineTextComponent.self) + let discountBackground = Child(RoundedRectangle.self) + let discount = Child(MultilineTextComponent.self) + let subtitle = Child(MultilineTextComponent.self) + let label = Child(MultilineTextComponent.self) + let selection = Child(RoundedRectangle.self) + + return { context in + let component = context.component + + let insets = UIEdgeInsets(top: 9.0, left: 62.0, bottom: 12.0, right: 16.0) + + let spacing: CGFloat = 2.0 + + let label = label.update( + component: MultilineTextComponent( + text: .plain( + NSAttributedString( + string: component.totalPrice, + font: Font.regular(17), + textColor: component.secondaryTextColor + ) + ), + maximumNumberOfLines: 1 + ), + availableSize: context.availableSize, + transition: context.transition + ) + + let title = title.update( + component: MultilineTextComponent( + text: .plain( + NSAttributedString( + string: component.title, + font: Font.regular(17), + textColor: component.primaryTextColor + ) + ), + maximumNumberOfLines: 1 + ), + availableSize: CGSize(width: context.availableSize.width - insets.left - insets.right - label.size.width, height: context.availableSize.height), + transition: context.transition + ) + + let discount = discount.update( + component: MultilineTextComponent( + text: .plain( + NSAttributedString( + string: component.discount, + font: Font.with(size: 14.0, design: .round, weight: .semibold, traits: []), + textColor: .white + ) + ), + maximumNumberOfLines: 1 + ), + availableSize: context.availableSize, + transition: context.transition + ) + + let discountSize = CGSize(width: discount.size.width + 6.0, height: 18.0) + + let discountBackground = discountBackground.update( + component: RoundedRectangle( + color: component.accentColor, + cornerRadius: 5.0 + ), + availableSize: discountSize, + transition: context.transition + ) + + let subtitle = subtitle.update( + component: MultilineTextComponent( + text: .plain( + NSAttributedString( + string: component.perMonthPrice, + font: Font.regular(13), + textColor: component.secondaryTextColor + ) + ), + maximumNumberOfLines: 1 + ), + availableSize: CGSize(width: context.availableSize.width - insets.left - insets.right - label.size.width - discountSize.width, height: context.availableSize.height), + transition: context.transition + ) + + let check = check.update( + component: CheckComponent( + theme: CheckComponent.Theme( + backgroundColor: component.accentColor, + strokeColor: component.checkForegroundColor, + borderColor: component.checkBorderColor, + overlayBorder: false, + hasInset: false, + hasShadow: false + ), + selected: component.selected + ), + availableSize: context.availableSize, + transition: context.transition + ) + + context.add(title + .position(CGPoint(x: insets.left + title.size.width / 2.0, y: insets.top + title.size.height / 2.0)) + ) + + context.add(discountBackground + .position(CGPoint(x: insets.left + discountSize.width / 2.0, y: insets.top + title.size.height + spacing + discountSize.height / 2.0)) + ) + + context.add(discount + .position(CGPoint(x: insets.left + discountSize.width / 2.0, y: insets.top + title.size.height + spacing + discountSize.height / 2.0)) + ) + + context.add(subtitle + .position(CGPoint(x: insets.left + discountSize.width + 7.0 + subtitle.size.width / 2.0, y: insets.top + title.size.height + spacing + discountSize.height / 2.0)) + ) + + let size = CGSize(width: context.availableSize.width, height: insets.top + title.size.height + spacing + subtitle.size.height + insets.bottom) + context.add(label + .position(CGPoint(x: context.availableSize.width - insets.right - label.size.width / 2.0, y: size.height / 2.0)) + ) + + context.add(check + .position(CGPoint(x: 20.0 + check.size.width / 2.0, y: size.height / 2.0)) + ) + + if component.selected { + let selection = selection.update( + component: RoundedRectangle( + color: component.accentColor, + cornerRadius: 10.0, + stroke: 2.0 + ), + availableSize: size, + transition: context.transition + ) + context.add(selection + .position(CGPoint(x: size.width / 2.0, y: size.height / 2.0)) + ) + } + + return size + } + } +} + +private final class CheckComponent: Component { + struct Theme: Equatable { + public let backgroundColor: UIColor + public let strokeColor: UIColor + public let borderColor: UIColor + public let overlayBorder: Bool + public let hasInset: Bool + public let hasShadow: Bool + public let filledBorder: Bool + public let borderWidth: CGFloat? + + public init(backgroundColor: UIColor, strokeColor: UIColor, borderColor: UIColor, overlayBorder: Bool, hasInset: Bool, hasShadow: Bool, filledBorder: Bool = false, borderWidth: CGFloat? = nil) { + self.backgroundColor = backgroundColor + self.strokeColor = strokeColor + self.borderColor = borderColor + self.overlayBorder = overlayBorder + self.hasInset = hasInset + self.hasShadow = hasShadow + self.filledBorder = filledBorder + self.borderWidth = borderWidth + } + + var checkNodeTheme: CheckNodeTheme { + return CheckNodeTheme( + backgroundColor: self.backgroundColor, + strokeColor: self.strokeColor, + borderColor: self.borderColor, + overlayBorder: self.overlayBorder, + hasInset: self.hasInset, + hasShadow: self.hasShadow, + filledBorder: self.filledBorder, + borderWidth: self.borderWidth + ) + } + } + + let theme: Theme + let selected: Bool + + init( + theme: Theme, + selected: Bool + ) { + self.theme = theme + self.selected = selected + } + + static func ==(lhs: CheckComponent, rhs: CheckComponent) -> Bool { + if lhs.theme != rhs.theme { + return false + } + if lhs.selected != rhs.selected { + return false + } + return true + } + + final class View: UIView { + private var currentValue: CGFloat? + private var animator: DisplayLinkAnimator? + + private var checkLayer: CheckLayer { + return self.layer as! CheckLayer + } + + override class var layerClass: AnyClass { + return CheckLayer.self + } + + init() { + super.init(frame: CGRect()) + } + + required init?(coder aDecoder: NSCoder) { + preconditionFailure() + } + + + func update(component: CheckComponent, availableSize: CGSize, transition: Transition) -> CGSize { + self.checkLayer.setSelected(component.selected, animated: true) + self.checkLayer.theme = component.theme.checkNodeTheme + + return CGSize(width: 22.0, height: 22.0) + } + } + + func makeView() -> View { + return View() + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, transition: transition) + } +} + +private final class PremiumGiftScreenContentComponent: CombinedComponent { + typealias EnvironmentType = (ViewControllerComponentContainer.Environment, ScrollChildEnvironment) + + let context: AccountContext + let peer: EnginePeer + let products: [InAppPurchaseManager.Product] + let selectedProductId: String + + let present: (ViewController) -> Void + let selectProduct: (String) -> Void + + init(context: AccountContext, peer: EnginePeer, products: [InAppPurchaseManager.Product], selectedProductId: String, present: @escaping (ViewController) -> Void, selectProduct: @escaping (String) -> Void) { + self.context = context + self.peer = peer + self.products = products + self.selectedProductId = selectedProductId + self.present = present + self.selectProduct = selectProduct + } + + static func ==(lhs: PremiumGiftScreenContentComponent, rhs: PremiumGiftScreenContentComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.peer != rhs.peer { + return false + } + if lhs.products != rhs.products { + return false + } + if lhs.selectedProductId != rhs.selectedProductId { + return false + } + + return true + } + + static var body: Body { + let overscroll = Child(Rectangle.self) + let fade = Child(RoundedRectangle.self) + let text = Child(MultilineTextComponent.self) + let section = Child(ProductGroupComponent.self) + let termsText = Child(MultilineTextComponent.self) + + return { context in + let sideInset: CGFloat = 16.0 + + let component = context.component + + let scrollEnvironment = context.environment[ScrollChildEnvironment.self].value + let environment = context.environment[ViewControllerComponentContainer.Environment.self].value + + let theme = environment.theme + let strings = environment.strings + + let availableWidth = context.availableSize.width + let sideInsets = sideInset * 2.0 + environment.safeInsets.left + environment.safeInsets.right + var size = CGSize(width: context.availableSize.width, height: 0.0) + + let overscroll = overscroll.update( + component: Rectangle(color: theme.list.plainBackgroundColor), + availableSize: CGSize(width: context.availableSize.width, height: 1000), + transition: context.transition + ) + context.add(overscroll + .position(CGPoint(x: overscroll.size.width / 2.0, y: -overscroll.size.height / 2.0)) + ) + + let fade = fade.update( + component: RoundedRectangle( + colors: [ + theme.list.plainBackgroundColor, + theme.list.blocksBackgroundColor + ], + cornerRadius: 0.0, + gradientDirection: .vertical + ), + availableSize: CGSize(width: availableWidth, height: 300), + transition: context.transition + ) + context.add(fade + .position(CGPoint(x: fade.size.width / 2.0, y: fade.size.height / 2.0)) + ) + + size.height += 183.0 + 10.0 + environment.navigationHeight - 56.0 + + let textColor = theme.list.itemPrimaryTextColor + let subtitleColor = theme.list.itemSecondaryTextColor +// let arrowColor = theme.list.disclosureArrowColor + + let textFont = Font.regular(15.0) + let boldTextFont = Font.semibold(15.0) + + let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: textColor), linkAttribute: { _ in + return nil + }) + let text = text.update( + component: MultilineTextComponent( + text: .markdown( + text: strings.Premium_Gift_Description(component.peer.compactDisplayTitle).string, + attributes: markdownAttributes + ), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.2 + ), + environment: {}, + availableSize: CGSize(width: availableWidth - sideInsets, height: 240.0), + transition: context.transition + ) + context.add(text + .position(CGPoint(x: size.width / 2.0, y: size.height + text.size.height / 2.0)) + ) + size.height += text.size.height + size.height += 21.0 + + var items: [ProductGroupComponent.Item] = [] + + let gradientColors: [UIColor] = [ + UIColor(rgb: 0x8e77ff), + UIColor(rgb: 0x9a6fff), + UIColor(rgb: 0xb36eee) + ] + + var i = 0 + for product in component.products { + let monthsCount: Int + let giftTitle: String + let discount: String + switch product.id { + case "org.telegram.telegramPremium.twelveMonths": + giftTitle = strings.Premium_Gift_Years(1) + monthsCount = 12 + discount = "-15%" + case "org.telegram.telegramPremium.sixMonths": + giftTitle = strings.Premium_Gift_Months(6) + monthsCount = 6 + discount = "-10%" + case "org.telegram.telegramPremium.threeMonths": + giftTitle = strings.Premium_Gift_Months(3) + monthsCount = 3 + discount = "-7%" + default: + giftTitle = "" + monthsCount = 1 + discount = "" + } + + items.append(ProductGroupComponent.Item( + AnyComponentWithIdentity( + id: product.id, + component: AnyComponent( + GiftComponent( + title: giftTitle, + totalPrice: product.price, + perMonthPrice: strings.Premium_Gift_PricePerMonth(product.pricePerMonth(monthsCount)).string, + discount: discount, + selected: product.id == component.selectedProductId, + primaryTextColor: textColor, + secondaryTextColor: subtitleColor, + accentColor: gradientColors[i], + checkForegroundColor: environment.theme.list.itemCheckColors.foregroundColor, + checkBorderColor: environment.theme.list.itemCheckColors.strokeColor + ) + ) + ), + action: { + component.selectProduct(product.id) + }) + ) + i += 1 + } + + let section = section.update( + component: ProductGroupComponent( + items: items, + backgroundColor: environment.theme.list.itemBlocksBackgroundColor, + selectionColor: environment.theme.list.itemHighlightedBackgroundColor + ), + environment: {}, + availableSize: CGSize(width: availableWidth - sideInsets, height: .greatestFiniteMagnitude), + transition: context.transition + ) + context.add(section + .position(CGPoint(x: availableWidth / 2.0, y: size.height + section.size.height / 2.0)) + .clipsToBounds(true) + .cornerRadius(10.0) + ) + size.height += section.size.height + size.height += 23.0 + + let textSideInset: CGFloat = 16.0 + + let termsFont = Font.regular(13.0) + let termsTextColor = environment.theme.list.freeTextColor + let termsMarkdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: termsFont, textColor: termsTextColor), bold: MarkdownAttributeSet(font: termsFont, textColor: termsTextColor), link: MarkdownAttributeSet(font: termsFont, textColor: environment.theme.list.itemAccentColor), linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + }) + + let termsString: MultilineTextComponent.TextContent = .markdown( + text: strings.Premium_Gift_Info, + attributes: termsMarkdownAttributes + ) + + let accountContext = component.context + let peerId = component.peer.id + let present = component.present + + let termsText = termsText.update( + component: MultilineTextComponent( + text: termsString, + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.0, + highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.3), + 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 { + let controller = PremiumIntroScreen(context: accountContext, source: .profile(peerId)) + present(controller) + } + } + ), + environment: {}, + availableSize: CGSize(width: availableWidth - sideInsets - textSideInset * 2.0, height: .greatestFiniteMagnitude), + transition: context.transition + ) +// context.add(termsText +// .position(CGPoint(x: sideInset + environment.safeInsets.left + textSideInset + termsText.size.width / 2.0, y: size.height + termsText.size.height / 2.0)) +// ) + context.add(termsText + .position(CGPoint(x: sideInset + environment.safeInsets.left + textSideInset + termsText.size.width / 2.0, y: size.height + 164.0 + termsText.size.height / 2.0)) + ) + size.height += termsText.size.height + size.height += 10.0 + size.height += scrollEnvironment.insets.bottom + + return size + } + } +} + +private final class PremiumGiftScreenComponent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let peerId: PeerId + let updateInProgress: (Bool) -> Void + let present: (ViewController) -> Void + let push: (ViewController) -> Void + let completion: () -> Void + + init(context: AccountContext, peerId: PeerId, updateInProgress: @escaping (Bool) -> Void, present: @escaping (ViewController) -> Void, push: @escaping (ViewController) -> Void, completion: @escaping () -> Void) { + self.context = context + self.peerId = peerId + self.updateInProgress = updateInProgress + self.present = present + self.push = push + self.completion = completion + } + + static func ==(lhs: PremiumGiftScreenComponent, rhs: PremiumGiftScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.peerId != rhs.peerId { + return false + } + return true + } + + final class State: ComponentState { + private let context: AccountContext + private let peerId: PeerId + private let updateInProgress: (Bool) -> Void + private let present: (ViewController) -> Void + private let completion: () -> Void + + var topContentOffset: CGFloat? + var bottomContentOffset: CGFloat? + + var hasIdleAnimations = true + + var inProgress = false + + var peer: EnginePeer? + var products: [InAppPurchaseManager.Product]? + var selectedProductId: String? + + private var disposable: Disposable? + private var paymentDisposable = MetaDisposable() + private var activationDisposable = MetaDisposable() + + init(context: AccountContext, peerId: PeerId, updateInProgress: @escaping (Bool) -> Void, present: @escaping (ViewController) -> Void, completion: @escaping () -> Void) { + self.context = context + self.peerId = peerId + self.updateInProgress = updateInProgress + self.present = present + self.completion = completion + + super.init() + + let availableProducts: Signal<[InAppPurchaseManager.Product], NoError> + if let inAppPurchaseManager = context.inAppPurchaseManager { + availableProducts = inAppPurchaseManager.availableProducts + } else { + availableProducts = .single([]) + } + + self.disposable = combineLatest( + queue: Queue.mainQueue(), + availableProducts, + context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + ).start(next: { [weak self] products, peer in + if let strongSelf = self { + strongSelf.products = products.filter { !$0.isSubscription }.sorted(by: { $0.priceValue.compare($1.priceValue) == .orderedDescending }) + strongSelf.selectedProductId = strongSelf.products?.first?.id + strongSelf.peer = peer + strongSelf.updated(transition: .immediate) + } + }) + } + + deinit { + self.disposable?.dispose() + self.paymentDisposable.dispose() + self.activationDisposable.dispose() + } + + func selectProduct(id: String) { + self.selectedProductId = id + self.updated(transition: .immediate) + } + + func buy() { + guard let inAppPurchaseManager = self.context.inAppPurchaseManager, !self.inProgress else { + return + } + + guard let product = self.products?.first(where: { $0.id == self.selectedProductId }) else { + return + } + +// addAppLogEvent(postbox: self.context.account.postbox, type: "premium.promo_screen_accept") + + self.inProgress = true + self.updateInProgress(true) + self.updated(transition: .immediate) + + let _ = (self.context.engine.payments.canPurchasePremium() + |> deliverOnMainQueue).start(next: { [weak self] available in + if let strongSelf = self { + if available { + strongSelf.paymentDisposable.set((inAppPurchaseManager.buyProduct(product) + |> 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) + |> castError(AssignAppStoreTransactionError.self) + |> take(until: { view in + if let peer = view.peers[view.peerId], peer.isPremium { + return SignalTakeAction(passthrough: false, complete: true) + } else { + return SignalTakeAction(passthrough: false, complete: false) + } + }) + |> mapToSignal { _ -> Signal in + 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) + strongSelf.completion() + + addAppLogEvent(postbox: strongSelf.context.account.postbox, type: "premium.promo_screen_fail") + + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + 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.updated(transition: .easeInOut(duration: 0.25)) + strongSelf.completion() + } + })) + } + }, error: { [weak self] error in + if let strongSelf = self { + strongSelf.inProgress = false + strongSelf.updateInProgress(false) + strongSelf.updated(transition: .immediate) + + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + 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 .cancelled: + break + } + + if let errorText = errorText { + addAppLogEvent(postbox: strongSelf.context.account.postbox, type: "premium.promo_screen_fail") + + let alertController = textAlertController(context: strongSelf.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) + strongSelf.present(alertController) + } + } + })) + } else { + strongSelf.inProgress = false + strongSelf.updateInProgress(false) + strongSelf.updated(transition: .immediate) + } + } + }) + } + + func updateIsFocused(_ isFocused: Bool) { + self.hasIdleAnimations = !isFocused + self.updated(transition: .immediate) + } + } + + func makeState() -> State { + return State(context: self.context, peerId: self.peerId, updateInProgress: self.updateInProgress, present: self.present, completion: self.completion) + } + + static var body: Body { + let background = Child(Rectangle.self) + let scrollContent = Child(ScrollComponent.self) + let star = Child(GiftAvatarComponent.self) + let topPanel = Child(BlurredRectangle.self) + let topSeparator = Child(Rectangle.self) + let title = Child(MultilineTextComponent.self) + let bottomPanel = Child(BlurredRectangle.self) + let bottomSeparator = Child(Rectangle.self) + let button = Child(SolidRoundedButtonComponent.self) + + return { context in + let environment = context.environment[EnvironmentType.self].value + let state = context.state + + let background = background.update(component: Rectangle(color: environment.theme.list.blocksBackgroundColor), environment: {}, availableSize: context.availableSize, transition: context.transition) + + var starIsVisible = true + if let topContentOffset = state.topContentOffset, topContentOffset >= 123.0 { + starIsVisible = false + } + + let topPanel = topPanel.update( + component: BlurredRectangle( + color: environment.theme.rootController.navigationBar.blurredBackgroundColor + ), + availableSize: CGSize(width: context.availableSize.width, height: environment.navigationHeight), + transition: context.transition + ) + + let topSeparator = topSeparator.update( + component: Rectangle( + color: environment.theme.rootController.navigationBar.separatorColor + ), + availableSize: CGSize(width: context.availableSize.width, height: UIScreenPixel), + transition: context.transition + ) + + let title = title.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString(string: environment.strings.Premium_Gift_Title, font: Font.bold(28.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)), + horizontalAlignment: .center, + truncationType: .end, + maximumNumberOfLines: 1 + ), + availableSize: context.availableSize, + transition: context.transition + ) + + let bottomPanelPadding: CGFloat = 12.0 + let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding + let bottomPanelHeight: CGFloat = bottomPanelPadding + 50.0 + bottomInset + + let topInset: CGFloat = environment.navigationHeight - 56.0 + + context.add(background + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + ) + + if let peer = state.peer, let products = state.products, let selectedProductId = state.selectedProductId { + let scrollContent = scrollContent.update( + component: ScrollComponent( + content: AnyComponent(PremiumGiftScreenContentComponent( + context: context.component.context, + peer: peer, + products: products, + selectedProductId: selectedProductId, + present: context.component.present, + selectProduct: { [weak state] productId in + state?.selectProduct(id: productId) + } + )), + contentInsets: UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: bottomPanelHeight, right: 0.0), + contentOffsetUpdated: { [weak state] topContentOffset, bottomContentOffset in + state?.topContentOffset = topContentOffset + state?.bottomContentOffset = bottomContentOffset + Queue.mainQueue().justDispatch { + state?.updated(transition: .immediate) + } + }, + contentOffsetWillCommit: { targetContentOffset in + if targetContentOffset.pointee.y < 100.0 { + targetContentOffset.pointee = CGPoint(x: 0.0, y: 0.0) + } else if targetContentOffset.pointee.y < 123.0 { + targetContentOffset.pointee = CGPoint(x: 0.0, y: 123.0) + } + } + ), + environment: { environment }, + availableSize: context.availableSize, + transition: context.transition + ) + + context.add(scrollContent + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + ) + } + + let topPanelAlpha: CGFloat + let titleOffset: CGFloat + let titleScale: CGFloat + let titleOffsetDelta = (topInset + 160.0) - (environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0) + let titleAlpha: CGFloat + + if let topContentOffset = state.topContentOffset { + topPanelAlpha = min(20.0, max(0.0, topContentOffset - 95.0)) / 20.0 + let topContentOffset = topContentOffset + max(0.0, min(1.0, topContentOffset / titleOffsetDelta)) * 10.0 + titleOffset = topContentOffset + let fraction = max(0.0, min(1.0, titleOffset / titleOffsetDelta)) + titleScale = 1.0 - fraction * 0.36 + titleAlpha = 1.0 + } else { + topPanelAlpha = 0.0 + titleScale = 1.0 + titleOffset = 0.0 + titleAlpha = 1.0 + } + + if let peer = context.state.peer { + let star = star.update( + component: GiftAvatarComponent( + context: context.component.context, + peer: peer, + isVisible: starIsVisible, + hasIdleAnimations: state.hasIdleAnimations + ), + availableSize: CGSize(width: min(390.0, context.availableSize.width), height: 220.0), + transition: context.transition + ) + + context.add(star + .position(CGPoint(x: context.availableSize.width / 2.0, y: topInset + star.size.height / 2.0 - 30.0 - titleOffset * titleScale)) + .scale(titleScale) + ) + } + + context.add(topPanel + .position(CGPoint(x: context.availableSize.width / 2.0, y: topPanel.size.height / 2.0)) + .opacity(topPanelAlpha) + ) + context.add(topSeparator + .position(CGPoint(x: context.availableSize.width / 2.0, y: topPanel.size.height)) + .opacity(topPanelAlpha) + ) + + context.add(title + .position(CGPoint(x: context.availableSize.width / 2.0, y: max(topInset + 160.0 - titleOffset, environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0))) + .scale(titleScale) + .opacity(titleAlpha) + ) + + let price: String? + if let products = state.products, let selectedProductId = state.selectedProductId, let product = products.first(where: { $0.id == selectedProductId }) { + price = product.price + } else { + price = nil + } + + let sideInset: CGFloat = 16.0 + let button = button.update( + component: SolidRoundedButtonComponent( + title: environment.strings.Premium_Gift_GiftSubscription(price ?? "—").string, + theme: SolidRoundedButtonComponent.Theme( + backgroundColor: UIColor(rgb: 0x8878ff), + backgroundColors: [ + UIColor(rgb: 0x0077ff), + UIColor(rgb: 0x6b93ff), + UIColor(rgb: 0x8878ff), + UIColor(rgb: 0xe46ace) + ], + foregroundColor: .white + ), + height: 50.0, + cornerRadius: 11.0, + gloss: true, + isLoading: state.inProgress, + action: { + state.buy() + } + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - environment.safeInsets.left - environment.safeInsets.right, height: 50.0), + transition: context.transition) + + let bottomPanel = bottomPanel.update( + component: BlurredRectangle( + color: environment.theme.rootController.tabBar.backgroundColor + ), + availableSize: CGSize(width: context.availableSize.width, height: bottomPanelPadding + button.size.height + bottomInset), + transition: context.transition + ) + + let bottomSeparator = bottomSeparator.update( + component: Rectangle( + color: environment.theme.rootController.tabBar.separatorColor + ), + availableSize: CGSize(width: context.availableSize.width, height: UIScreenPixel), + transition: context.transition + ) + + let bottomPanelAlpha: CGFloat + if let bottomContentOffset = state.bottomContentOffset { + bottomPanelAlpha = min(16.0, bottomContentOffset) / 16.0 + } else { + bottomPanelAlpha = 0.0 + } + + context.add(bottomPanel + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanel.size.height / 2.0)) + .opacity(bottomPanelAlpha) + .disappear(Transition.Disappear { view, transition, completion in + if case .none = transition.animation { + completion() + return + } + view.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: bottomPanel.size.height), duration: 0.2, removeOnCompletion: false, additive: true, completion: { _ in + completion() + }) + }) + ) + context.add(bottomSeparator + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanel.size.height)) + .opacity(bottomPanelAlpha) + .disappear(Transition.Disappear { view, transition, completion in + if case .none = transition.animation { + completion() + return + } + view.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: bottomPanel.size.height), duration: 0.2, removeOnCompletion: false, additive: true, completion: { _ in + completion() + }) + }) + ) + context.add(button + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanel.size.height + bottomPanelPadding + button.size.height / 2.0)) + .disappear(Transition.Disappear { view, transition, completion in + if case .none = transition.animation { + completion() + return + } + view.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: bottomPanel.size.height), duration: 0.2, removeOnCompletion: false, additive: true, completion: { _ in + completion() + }) + }) + ) + + return context.availableSize + } + } +} + +public final class PremiumGiftScreen: ViewControllerComponentContainer { + fileprivate let context: AccountContext + + private var didSetReady = false + private let _ready = Promise() + public override var ready: Promise { + return self._ready + } + + public weak var sourceView: UIView? + public weak var containerView: UIView? + public var animationColor: UIColor? + + public init(context: AccountContext, peerId: PeerId) { + self.context = context + + var updateInProgressImpl: ((Bool) -> Void)? + var pushImpl: ((ViewController) -> Void)? +// var presentImpl: ((ViewController) -> Void)? + var completionImpl: (() -> Void)? + super.init(context: context, component: PremiumGiftScreenComponent( + context: context, + peerId: peerId, + updateInProgress: { inProgress in + updateInProgressImpl?(inProgress) + }, + present: { c in + pushImpl?(c) + }, + push: { c in + pushImpl?(c) + }, + completion: { + completionImpl?() + } + ), navigationBarAppearance: .transparent) + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + let cancelItem = UIBarButtonItem(title: presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) + self.navigationItem.setLeftBarButton(cancelItem, animated: false) + self.navigationPresentation = .modal + + updateInProgressImpl = { [weak self] inProgress in + if let strongSelf = self { + strongSelf.navigationItem.leftBarButtonItem?.isEnabled = !inProgress + strongSelf.view.disablesInteractiveTransitionGestureRecognizer = inProgress + strongSelf.view.disablesInteractiveModalDismiss = inProgress + } + } + +// presentImpl = { [weak self] c in +// self?.present(c, in: .window(.root)) +// } + + pushImpl = { [weak self] c in + self?.push(c) + } + + completionImpl = { [weak self] in + if let strongSelf = self { + strongSelf.view.addSubview(ConfettiView(frame: strongSelf.view.bounds)) + } + } + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func cancelPressed() { + self.dismiss() + } + + public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + if !self.didSetReady { + self.didSetReady = true + if let view = self.node.hostView.findTaggedView(tag: GiftAvatarComponent.View.Tag()) as? GiftAvatarComponent.View { + self._ready.set(view.ready) + } else { + self._ready.set(.single(true)) + } + } + } +} diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index 69301eaacd..c62048a366 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -11,7 +11,6 @@ import ViewControllerComponent import AccountContext import SolidRoundedButtonComponent import MultilineTextComponent -import PrefixSectionGroupComponent import BundleIconComponent import SolidRoundedButtonComponent import Markdown @@ -470,135 +469,6 @@ private final class SectionGroupComponent: Component { } } - -private final class ScrollChildEnvironment: Equatable { - public let insets: UIEdgeInsets - - public init(insets: UIEdgeInsets) { - self.insets = insets - } - - public static func ==(lhs: ScrollChildEnvironment, rhs: ScrollChildEnvironment) -> Bool { - if lhs.insets != rhs.insets { - return false - } - - return true - } -} - -private final class ScrollComponent: Component { - public typealias EnvironmentType = ChildEnvironment - - public let content: AnyComponent<(ChildEnvironment, ScrollChildEnvironment)> - public let contentInsets: UIEdgeInsets - public let contentOffsetUpdated: (_ top: CGFloat, _ bottom: CGFloat) -> Void - public let contentOffsetWillCommit: (UnsafeMutablePointer) -> Void - - public init( - content: AnyComponent<(ChildEnvironment, ScrollChildEnvironment)>, - contentInsets: UIEdgeInsets, - contentOffsetUpdated: @escaping (_ top: CGFloat, _ bottom: CGFloat) -> Void, - contentOffsetWillCommit: @escaping (UnsafeMutablePointer) -> Void - ) { - self.content = content - self.contentInsets = contentInsets - self.contentOffsetUpdated = contentOffsetUpdated - self.contentOffsetWillCommit = contentOffsetWillCommit - } - - public static func ==(lhs: ScrollComponent, rhs: ScrollComponent) -> Bool { - if lhs.content != rhs.content { - return false - } - if lhs.contentInsets != rhs.contentInsets { - return false - } - - return true - } - - public final class View: UIScrollView, UIScrollViewDelegate { - private var component: ScrollComponent? - private let contentView: ComponentHostView<(ChildEnvironment, ScrollChildEnvironment)> - - override init(frame: CGRect) { - self.contentView = ComponentHostView() - - super.init(frame: frame) - - if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { - self.contentInsetAdjustmentBehavior = .never - } - self.delegate = self - self.showsVerticalScrollIndicator = false - self.showsHorizontalScrollIndicator = false - self.canCancelContentTouches = true - - self.addSubview(self.contentView) - } - - public override func touchesShouldCancel(in view: UIView) -> Bool { - return true - } - - private var ignoreDidScroll = false - public func scrollViewDidScroll(_ scrollView: UIScrollView) { - guard let component = self.component, !self.ignoreDidScroll else { - return - } - let topOffset = scrollView.contentOffset.y - let bottomOffset = max(0.0, scrollView.contentSize.height - scrollView.contentOffset.y - scrollView.frame.height) - component.contentOffsetUpdated(topOffset, bottomOffset) - } - - public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { - guard let component = self.component, !self.ignoreDidScroll else { - return - } - component.contentOffsetWillCommit(targetContentOffset) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func update(component: ScrollComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { - let contentSize = self.contentView.update( - transition: transition, - component: component.content, - environment: { - environment[ChildEnvironment.self] - ScrollChildEnvironment(insets: component.contentInsets) - }, - containerSize: CGSize(width: availableSize.width, height: .greatestFiniteMagnitude) - ) - transition.setFrame(view: self.contentView, frame: CGRect(origin: .zero, size: contentSize), completion: nil) - - if self.contentSize != contentSize { - self.ignoreDidScroll = true - self.contentSize = contentSize - self.ignoreDidScroll = false - } - if self.scrollIndicatorInsets != component.contentInsets { - self.scrollIndicatorInsets = component.contentInsets - } - - self.component = component - - return availableSize - } - } - - public func makeView() -> View { - return View(frame: CGRect()) - } - - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { - return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) - } -} - private final class PerkComponent: CombinedComponent { let iconName: String let iconBackgroundColors: [UIColor] @@ -1379,7 +1249,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { otherPeerName ).start(next: { [weak self] products, isPremium, otherPeerName in if let strongSelf = self { - strongSelf.premiumProduct = products.first + strongSelf.premiumProduct = products.first(where: { $0.isSubscription }) strongSelf.isPremium = isPremium strongSelf.otherPeerName = otherPeerName strongSelf.updated(transition: .immediate) diff --git a/submodules/PremiumUI/Sources/PremiumStarComponent.swift b/submodules/PremiumUI/Sources/PremiumStarComponent.swift index 9ec5fe42b9..ce5558ca11 100644 --- a/submodules/PremiumUI/Sources/PremiumStarComponent.swift +++ b/submodules/PremiumUI/Sources/PremiumStarComponent.swift @@ -410,7 +410,7 @@ class PremiumStarComponent: Component { if explode, let node = scene.rootNode.childNode(withName: "swirl", recursively: false), let particles = scene.rootNode.childNode(withName: "particles", recursively: false) { if let particleSystem = particles.particleSystems?.first { - particleSystem.particleColorVariation = SCNVector4(0.15, 0.2, 0.35, 0.3) + particleSystem.particleColorVariation = SCNVector4(0.15, 0.2, 0.15, 0.3) particleSystem.speedFactor = 2.0 particleSystem.particleVelocity = 2.2 particleSystem.birthRate = 4.0 diff --git a/submodules/PremiumUI/Sources/ScrollComponent.swift b/submodules/PremiumUI/Sources/ScrollComponent.swift new file mode 100644 index 0000000000..a0797ca1d0 --- /dev/null +++ b/submodules/PremiumUI/Sources/ScrollComponent.swift @@ -0,0 +1,132 @@ +import Foundation +import UIKit +import ComponentFlow +import Display + +final class ScrollChildEnvironment: Equatable { + public let insets: UIEdgeInsets + + public init(insets: UIEdgeInsets) { + self.insets = insets + } + + public static func ==(lhs: ScrollChildEnvironment, rhs: ScrollChildEnvironment) -> Bool { + if lhs.insets != rhs.insets { + return false + } + + return true + } +} + +final class ScrollComponent: Component { + public typealias EnvironmentType = ChildEnvironment + + public let content: AnyComponent<(ChildEnvironment, ScrollChildEnvironment)> + public let contentInsets: UIEdgeInsets + public let contentOffsetUpdated: (_ top: CGFloat, _ bottom: CGFloat) -> Void + public let contentOffsetWillCommit: (UnsafeMutablePointer) -> Void + + public init( + content: AnyComponent<(ChildEnvironment, ScrollChildEnvironment)>, + contentInsets: UIEdgeInsets, + contentOffsetUpdated: @escaping (_ top: CGFloat, _ bottom: CGFloat) -> Void, + contentOffsetWillCommit: @escaping (UnsafeMutablePointer) -> Void + ) { + self.content = content + self.contentInsets = contentInsets + self.contentOffsetUpdated = contentOffsetUpdated + self.contentOffsetWillCommit = contentOffsetWillCommit + } + + public static func ==(lhs: ScrollComponent, rhs: ScrollComponent) -> Bool { + if lhs.content != rhs.content { + return false + } + if lhs.contentInsets != rhs.contentInsets { + return false + } + + return true + } + + public final class View: UIScrollView, UIScrollViewDelegate { + private var component: ScrollComponent? + private let contentView: ComponentHostView<(ChildEnvironment, ScrollChildEnvironment)> + + override init(frame: CGRect) { + self.contentView = ComponentHostView() + + super.init(frame: frame) + + if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { + self.contentInsetAdjustmentBehavior = .never + } + self.delegate = self + self.showsVerticalScrollIndicator = false + self.showsHorizontalScrollIndicator = false + self.canCancelContentTouches = true + + self.addSubview(self.contentView) + } + + public override func touchesShouldCancel(in view: UIView) -> Bool { + return true + } + + private var ignoreDidScroll = false + public func scrollViewDidScroll(_ scrollView: UIScrollView) { + guard let component = self.component, !self.ignoreDidScroll else { + return + } + let topOffset = scrollView.contentOffset.y + let bottomOffset = max(0.0, scrollView.contentSize.height - scrollView.contentOffset.y - scrollView.frame.height) + component.contentOffsetUpdated(topOffset, bottomOffset) + } + + public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { + guard let component = self.component, !self.ignoreDidScroll else { + return + } + component.contentOffsetWillCommit(targetContentOffset) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: ScrollComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let contentSize = self.contentView.update( + transition: transition, + component: component.content, + environment: { + environment[ChildEnvironment.self] + ScrollChildEnvironment(insets: component.contentInsets) + }, + containerSize: CGSize(width: availableSize.width, height: .greatestFiniteMagnitude) + ) + transition.setFrame(view: self.contentView, frame: CGRect(origin: .zero, size: contentSize), completion: nil) + + if self.contentSize != contentSize { + self.ignoreDidScroll = true + self.contentSize = contentSize + self.ignoreDidScroll = false + } + if self.scrollIndicatorInsets != component.contentInsets { + self.scrollIndicatorInsets = component.contentInsets + } + + self.component = component + + return availableSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/SettingsUI/BUILD b/submodules/SettingsUI/BUILD index 8dd9f3c616..3e2c5fb18d 100644 --- a/submodules/SettingsUI/BUILD +++ b/submodules/SettingsUI/BUILD @@ -101,6 +101,7 @@ swift_library( "//submodules/ListMessageItem:ListMessageItem", "//submodules/PaymentMethodUI:PaymentMethodUI", "//submodules/PremiumUI:PremiumUI", + "//submodules/InviteLinksUI:InviteLinksUI", ], visibility = [ "//visibility:public", diff --git a/submodules/SettingsUI/Sources/DeleteAccountDataController.swift b/submodules/SettingsUI/Sources/DeleteAccountDataController.swift new file mode 100644 index 0000000000..9c1886c473 --- /dev/null +++ b/submodules/SettingsUI/Sources/DeleteAccountDataController.swift @@ -0,0 +1,190 @@ +import Foundation +import UIKit +import Display +import SwiftSignalKit +import Postbox +import TelegramCore +import LegacyComponents +import TelegramPresentationData +import ItemListUI +import PresentationDataUtils +import AccountContext +import AlertUI +import PresentationDataUtils +import UrlHandling +import InviteLinksUI + +private struct DeleteAccountDataArguments { + let context: AccountContext + let openLink: (String) -> Void +} + +private enum DeleteAccountDataSection: Int32 { + case main +} + +private enum DeleteAccountDataEntry: ItemListNodeEntry, Equatable { + case header(PresentationTheme, String, String, String) + + case peers(PresentationTheme, [Peer]) + case info(PresentationTheme, String) + + var section: ItemListSectionId { + switch self { + case .header, .peers, .info: + return DeleteAccountDataSection.main.rawValue + } + } + + var stableId: Int32 { + switch self { + case .header: + return 0 + case .peers: + return 1 + case .info: + return 3 + } + } + + static func == (lhs: DeleteAccountDataEntry, rhs: DeleteAccountDataEntry) -> Bool { + switch lhs { + case let .header(lhsTheme, lhsAnimation, lhsTitle, lhsText): + if case let .header(rhsTheme, rhsAnimation, rhsTitle, rhsText) = rhs, lhsTheme === rhsTheme, lhsAnimation == rhsAnimation, lhsTitle == rhsTitle, lhsText == rhsText { + return true + } else { + return false + } + case let .peers(lhsTheme, lhsPeers): + if case let .peers(rhsTheme, rhsPeers) = rhs, lhsTheme === rhsTheme, arePeerArraysEqual(lhsPeers, rhsPeers) { + return true + } else { + return false + } + case let .info(lhsTheme, lhsText): + if case let .info(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + } + } + + static func <(lhs: DeleteAccountDataEntry, rhs: DeleteAccountDataEntry) -> Bool { + return lhs.stableId < rhs.stableId + } + + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { + let arguments = arguments as! DeleteAccountDataArguments + switch self { + case let .header(theme, animation, title, text): + return InviteLinkHeaderItem(context: arguments.context, theme: theme, title: title, text: text, animationName: animation, sectionId: self.section, linkAction: nil) + case let .peers(_, peers): + return ItemListTextItem(presentationData: presentationData, text: .plain(peers.first?.debugDisplayTitle ?? ""), sectionId: self.section) + case let .info(_, text): + return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section) + } + } +} + +private func deleteAccountDataEntries(presentationData: PresentationData, mode: DeleteAccountDataMode, peers: [Peer]) -> [DeleteAccountDataEntry] { + var entries: [DeleteAccountDataEntry] = [] + + let headerTitle: String + let headerText: String + let headerAnimation: String + + switch mode { + case .peers: + headerAnimation = "" + headerTitle = presentationData.strings.DeleteAccount_CloudStorageTitle + headerText = presentationData.strings.DeleteAccount_CloudStorageText + case .groups: + headerAnimation = "" + headerTitle = presentationData.strings.DeleteAccount_GroupsAndChannelsTitle + headerText = presentationData.strings.DeleteAccount_GroupsAndChannelsText + case .messages: + headerAnimation = "" + headerTitle = presentationData.strings.DeleteAccount_MessageHistoryTitle + headerText = presentationData.strings.DeleteAccount_MessageHistoryText + } + + entries.append(.header(presentationData.theme, headerAnimation, headerTitle, headerText)) + entries.append(.peers(presentationData.theme, peers)) + + if case .groups = mode { + entries.append(.info(presentationData.theme, presentationData.strings.DeleteAccount_GroupsAndChannelsInfo)) + } + + return entries +} + +enum DeleteAccountDataMode { + case peers + case groups + case messages +} + +func deleteAccountDataController(context: AccountContext, mode: DeleteAccountDataMode) -> ViewController { + var replaceTopControllerImpl: ((ViewController) -> Void)? + var dismissImpl: (() -> Void)? + + let arguments = DeleteAccountDataArguments(context: context, openLink: { _ in + + }) + + let peers: Signal<[Peer], NoError> = .single([]) + + let signal = combineLatest(queue: .mainQueue(), + context.sharedContext.presentationData, + peers + ) + |> map { presentationData, peers -> (ItemListControllerState, (ItemListNodeState, Any)) in + let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { + dismissImpl?() + }) + + let footerItem = DeleteAccountFooterItem(theme: presentationData.theme, title: presentationData.strings.DeleteAccount_ComeBackLater, secondaryTitle: presentationData.strings.DeleteAccount_Continue, action: { + dismissImpl?() + }, secondaryAction: { + let nextMode: DeleteAccountDataMode? + switch mode { + case .peers: + nextMode = .groups + case .groups: + nextMode = .messages + case .messages: + nextMode = nil + } + + if let nextMode = nextMode { + let controller = deleteAccountDataController(context: context, mode: nextMode) + replaceTopControllerImpl?(controller) + } + }) + + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.DeleteAccount_DeleteMyAccountTitle), leftNavigationButton: leftNavigationButton, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: deleteAccountDataEntries(presentationData: presentationData, mode: mode, peers: peers), style: .blocks, footerItem: footerItem) + + return (controllerState, (listState, arguments)) + } + + let controller = ItemListController(context: context, state: signal, tabBarItem: nil) + replaceTopControllerImpl = { [weak controller] c in + if let navigationController = controller?.navigationController as? NavigationController { + navigationController.pushViewController(c, completion: { [weak navigationController, weak controller, weak c] in + if let navigationController = navigationController { + let controllers = navigationController.viewControllers.filter { $0 !== controller } + c?.navigationPresentation = .modal + navigationController.setViewControllers(controllers, animated: false) + } + }) + } + } + dismissImpl = { [weak controller] in + let _ = controller?.dismiss() + } + + return controller +} + diff --git a/submodules/SettingsUI/Sources/DeleteAccountFooterItem.swift b/submodules/SettingsUI/Sources/DeleteAccountFooterItem.swift new file mode 100644 index 0000000000..208bd12c34 --- /dev/null +++ b/submodules/SettingsUI/Sources/DeleteAccountFooterItem.swift @@ -0,0 +1,152 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import TelegramPresentationData +import ItemListUI +import PresentationDataUtils +import SolidRoundedButtonNode +import AppBundle + +final class DeleteAccountFooterItem: ItemListControllerFooterItem { + let theme: PresentationTheme + let title: String + let secondaryTitle: String + let action: () -> Void + let secondaryAction: () -> Void + + init(theme: PresentationTheme, title: String, secondaryTitle: String, action: @escaping () -> Void, secondaryAction: @escaping () -> Void) { + self.theme = theme + self.title = title + self.secondaryTitle = secondaryTitle + self.action = action + self.secondaryAction = secondaryAction + } + + func isEqual(to: ItemListControllerFooterItem) -> Bool { + if let item = to as? DeleteAccountFooterItem { + return self.theme === item.theme && self.title == item.title && self.secondaryTitle == item.secondaryTitle + } else { + return false + } + } + + func node(current: ItemListControllerFooterItemNode?) -> ItemListControllerFooterItemNode { + if let current = current as? DeleteAccountFooterItemNode { + current.item = self + return current + } else { + return DeleteAccountFooterItemNode(item: self) + } + } +} + +final class DeleteAccountFooterItemNode: ItemListControllerFooterItemNode { + private let backgroundNode: NavigationBackgroundNode + private let separatorNode: ASDisplayNode + private let buttonNode: SolidRoundedButtonNode + private let secondaryButtonNode: HighlightTrackingButtonNode + + private var validLayout: ContainerViewLayout? + + var item: DeleteAccountFooterItem { + didSet { + self.updateItem() + if let layout = self.validLayout { + let _ = self.updateLayout(layout: layout, transition: .immediate) + } + } + } + + init(item: DeleteAccountFooterItem) { + self.item = item + + self.backgroundNode = NavigationBackgroundNode(color: item.theme.rootController.tabBar.backgroundColor) + self.separatorNode = ASDisplayNode() + + self.buttonNode = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(backgroundColor: .black, foregroundColor: .white), height: 50.0, cornerRadius: 11.0, gloss: true) + + self.secondaryButtonNode = HighlightTrackingButtonNode() + + super.init() + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.separatorNode) + self.addSubnode(self.buttonNode) + self.addSubnode(self.secondaryButtonNode) + + self.secondaryButtonNode.addTarget(self, action: #selector(self.secondaryButtonPressed), forControlEvents: .touchUpInside) + + self.updateItem() + } + + @objc private func secondaryButtonPressed() { + self.item.secondaryAction() + } + + private func updateItem() { + self.backgroundNode.updateColor(color: self.item.theme.rootController.tabBar.backgroundColor, transition: .immediate) + self.separatorNode.backgroundColor = self.item.theme.rootController.tabBar.separatorColor + + let backgroundColor = self.item.theme.list.itemCheckColors.fillColor + let textColor = self.item.theme.list.itemCheckColors.foregroundColor + + self.buttonNode.updateTheme(SolidRoundedButtonTheme(backgroundColor: backgroundColor, foregroundColor: textColor), animated: false) + self.buttonNode.title = self.item.title + + self.buttonNode.pressed = { [weak self] in + self?.item.action() + } + + self.secondaryButtonNode.setTitle(self.item.secondaryTitle, with: Font.regular(17.0), with: self.item.theme.list.itemAccentColor, for: .normal) + } + + override func updateBackgroundAlpha(_ alpha: CGFloat, transition: ContainedViewLayoutTransition) { + transition.updateAlpha(node: self.backgroundNode, alpha: alpha) + transition.updateAlpha(node: self.separatorNode, alpha: alpha) + } + + override func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) -> CGFloat { + self.validLayout = layout + + let buttonInset: CGFloat = 16.0 + let buttonWidth = layout.size.width - layout.safeInsets.left - layout.safeInsets.right - buttonInset * 2.0 + let buttonHeight = self.buttonNode.updateLayout(width: buttonWidth, transition: transition) + let topInset: CGFloat = 9.0 + let bottomInset: CGFloat = 23.0 + let spacing: CGFloat = 23.0 + + let insets = layout.insets(options: [.input]) + + let secondaryButtonSize = self.secondaryButtonNode.measure(CGSize(width: buttonWidth, height: CGFloat.greatestFiniteMagnitude)) + + var panelHeight: CGFloat = buttonHeight + topInset + spacing + secondaryButtonSize.height + bottomInset + let totalPanelHeight: CGFloat + if let inputHeight = layout.inputHeight, inputHeight > 0.0 { + totalPanelHeight = panelHeight + insets.bottom + } else { + panelHeight += insets.bottom + totalPanelHeight = panelHeight + } + + let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - totalPanelHeight), size: CGSize(width: layout.size.width, height: panelHeight)) + transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + buttonInset, y: panelFrame.minY + topInset), size: CGSize(width: buttonWidth, height: buttonHeight))) + + transition.updateFrame(node: self.secondaryButtonNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - secondaryButtonSize.width) / 2.0), y: panelFrame.minY + topInset + buttonHeight + spacing), size: secondaryButtonSize)) + + transition.updateFrame(node: self.backgroundNode, frame: panelFrame) + self.backgroundNode.update(size: panelFrame.size, transition: transition) + + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: panelFrame.origin, size: CGSize(width: panelFrame.width, height: UIScreenPixel))) + + return panelHeight + } + + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + if self.backgroundNode.frame.contains(point) { + return true + } else { + return false + } + } +} diff --git a/submodules/SettingsUI/Sources/DeleteAccountOptionsController.swift b/submodules/SettingsUI/Sources/DeleteAccountOptionsController.swift new file mode 100644 index 0000000000..7325104044 --- /dev/null +++ b/submodules/SettingsUI/Sources/DeleteAccountOptionsController.swift @@ -0,0 +1,349 @@ +import Foundation +import UIKit +import Display +import SwiftSignalKit +import Postbox +import TelegramCore +import LegacyComponents +import TelegramPresentationData +import ItemListUI +import PresentationDataUtils +import OverlayStatusController +import AccountContext +import AlertUI +import PresentationDataUtils +import UrlHandling +import AccountUtils +import PremiumUI + +private struct DeleteAccountOptionsArguments { + let changePhoneNumber: () -> Void + let addAccount: () -> Void + let setupPrivacy: () -> Void + let setTwoStepAuth: () -> Void + let setPasscode: () -> Void + let clearCache: () -> Void + let contactSupport: () -> Void + let deleteAccount: () -> Void +} + +private enum DeleteAccountOptionsSection: Int32 { + case add + case privacy + case remove + case support + case delete +} + +private enum DeleteAccountOptionsEntry: ItemListNodeEntry, Equatable { + case changePhoneNumber(PresentationTheme, String, String) + case addAccount(PresentationTheme, String, String) + + case changePrivacy(PresentationTheme, String, String) + case setTwoStepAuth(PresentationTheme, String, String) + case setPasscode(PresentationTheme, String, String) + + case clearCache(PresentationTheme, String, String) + case clearSyncedContacts(PresentationTheme, String, String) + case deleteChats(PresentationTheme, String, String) + + case contactSupport(PresentationTheme, String, String) + + case deleteAccount(PresentationTheme, String) + + var section: ItemListSectionId { + switch self { + case .changePhoneNumber, .addAccount: + return DeleteAccountOptionsSection.add.rawValue + case .changePrivacy, .setTwoStepAuth, .setPasscode: + return DeleteAccountOptionsSection.privacy.rawValue + case .clearCache, .clearSyncedContacts, .deleteChats: + return DeleteAccountOptionsSection.remove.rawValue + case .contactSupport: + return DeleteAccountOptionsSection.support.rawValue + case .deleteAccount: + return DeleteAccountOptionsSection.delete.rawValue + } + } + + var stableId: Int32 { + switch self { + case .changePhoneNumber: + return 0 + case .addAccount: + return 1 + case .changePrivacy: + return 2 + case .setTwoStepAuth: + return 3 + case .setPasscode: + return 4 + case .clearCache: + return 5 + case .clearSyncedContacts: + return 6 + case .deleteChats: + return 7 + case .contactSupport: + return 8 + case .deleteAccount: + return 9 + } + } + + static func <(lhs: DeleteAccountOptionsEntry, rhs: DeleteAccountOptionsEntry) -> Bool { + return lhs.stableId < rhs.stableId + } + + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { + let arguments = arguments as! DeleteAccountOptionsArguments + switch self { + case let .changePhoneNumber(_, title, text): + return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.changePhoneNumber, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + arguments.changePhoneNumber() + }) + case let .addAccount(_, title, text): + return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.deleteAddAccount, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + arguments.addAccount() + }) + case let .changePrivacy(_, title, text): + return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.security, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + arguments.setupPrivacy() + }) + case let .setTwoStepAuth(_, title, text): + return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.setPasscode, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + arguments.setTwoStepAuth() + }) + case let .setPasscode(_, title, text): + return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.deleteSetPasscode, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + arguments.setPasscode() + }) + case let .clearCache(_, title, text): + return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.dataAndStorage, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + arguments.clearCache() + }) + case let .clearSyncedContacts(_, title, text): + return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.clearSynced, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + arguments.clearCache() + }) + case let .deleteChats(_, title, text): + return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.deleteChats, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + arguments.clearCache() + }) + case let .contactSupport(_, title, text): + return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.support, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + arguments.contactSupport() + }) + case let .deleteAccount(_, title): + return ItemListActionItem(presentationData: presentationData, title: title, kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { + arguments.deleteAccount() + }) + } + } +} + +private func deleteAccountOptionsEntries(presentationData: PresentationData, canAddAccounts: Bool, hasTwoStepAuth: Bool, hasPasscode: Bool) -> [DeleteAccountOptionsEntry] { + var entries: [DeleteAccountOptionsEntry] = [] + + entries.append(.changePhoneNumber(presentationData.theme, presentationData.strings.DeleteAccount_Options_ChangePhoneNumberTitle, presentationData.strings.DeleteAccount_Options_ChangePhoneNumberText)) + if canAddAccounts { + entries.append(.addAccount(presentationData.theme, presentationData.strings.DeleteAccount_Options_AddAccountTitle, presentationData.strings.DeleteAccount_Options_AddAccountText)) + } + + entries.append(.changePrivacy(presentationData.theme, presentationData.strings.DeleteAccount_Options_ChangePrivacyTitle, presentationData.strings.DeleteAccount_Options_ChangePrivacyText)) + if !hasTwoStepAuth { + entries.append(.setTwoStepAuth(presentationData.theme, presentationData.strings.DeleteAccount_Options_SetTwoStepAuthTitle, presentationData.strings.DeleteAccount_Options_SetTwoStepAuthText)) + } + if !hasPasscode { + entries.append(.setPasscode(presentationData.theme, presentationData.strings.DeleteAccount_Options_SetPasscodeTitle, presentationData.strings.DeleteAccount_Options_SetPasscodeText)) + } + entries.append(.clearCache(presentationData.theme, presentationData.strings.DeleteAccount_Options_ClearCacheTitle, presentationData.strings.DeleteAccount_Options_ClearCacheText)) + entries.append(.clearSyncedContacts(presentationData.theme, presentationData.strings.DeleteAccount_Options_ClearSyncedContactsTitle, presentationData.strings.DeleteAccount_Options_ClearSyncedContactsText)) + entries.append(.deleteChats(presentationData.theme, presentationData.strings.DeleteAccount_Options_DeleteChatsTitle, presentationData.strings.DeleteAccount_Options_DeleteChatsText)) + + entries.append(.contactSupport(presentationData.theme, presentationData.strings.DeleteAccount_Options_ContactSupportTitle, presentationData.strings.DeleteAccount_Options_ContactSupportText)) + + entries.append(.deleteAccount(presentationData.theme, presentationData.strings.DeleteAccount_DeleteMyAccount)) + + return entries +} + +public func deleteAccountOptionsController(context: AccountContext, navigationController: NavigationController, hasTwoStepAuth: Bool) -> ViewController { + var pushControllerImpl: ((ViewController) -> Void)? + var presentControllerImpl: ((ViewController, Any?) -> Void)? + var replaceTopControllerImpl: ((ViewController, Bool) -> Void)? + var dismissImpl: (() -> Void)? + + let supportPeerDisposable = MetaDisposable() + + let arguments = DeleteAccountOptionsArguments(changePhoneNumber: { + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.engine.account.peerId)) + |> deliverOnMainQueue).start(next: { accountPeer in + guard let accountPeer = accountPeer, case let .user(user) = accountPeer else { + return + } + let introController = PrivacyIntroController(context: context, mode: .changePhoneNumber(user.phone ?? ""), proceedAction: { + replaceTopControllerImpl?(ChangePhoneNumberController(context: context), false) + }) + pushControllerImpl?(introController) + dismissImpl?() + }) + }, addAccount: { + let _ = (activeAccountsAndPeers(context: context) + |> take(1) + |> deliverOnMainQueue + ).start(next: { accountAndPeer, accountsAndPeers in + var maximumAvailableAccounts: Int = 3 + if accountAndPeer?.1.isPremium == true && !context.account.testingEnvironment { + maximumAvailableAccounts = 4 + } + var count: Int = 1 + for (accountContext, peer, _) in accountsAndPeers { + if !accountContext.account.testingEnvironment { + if peer.isPremium { + maximumAvailableAccounts = 4 + } + count += 1 + } + } + + if count >= maximumAvailableAccounts { + var replaceImpl: ((ViewController) -> Void)? + let controller = PremiumLimitScreen(context: context, subject: .accounts, count: Int32(count), action: { + let controller = PremiumIntroScreen(context: context, source: .accounts) + replaceImpl?(controller) + }) + replaceImpl = { [weak controller] c in + controller?.replace(with: c) + } + pushControllerImpl?(controller) + } else { + context.sharedContext.beginNewAuth(testingEnvironment: context.account.testingEnvironment) + + dismissImpl?() + } + }) + }, setupPrivacy: { + + }, setTwoStepAuth: { + + }, setPasscode: { + let _ = passcodeOptionsAccessController(context: context, pushController: { controller in + replaceTopControllerImpl?(controller, false) + }, completion: { _ in + replaceTopControllerImpl?(passcodeOptionsController(context: context), false) + }).start(next: { controller in + if let controller = controller { + pushControllerImpl?(controller) + } + }) + dismissImpl?() + }, clearCache: { + pushControllerImpl?(storageUsageController(context: context)) + dismissImpl?() + }, contactSupport: { [weak navigationController] in + let supportPeer = Promise() + supportPeer.set(context.engine.peers.supportPeerId()) + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + var faqUrl = presentationData.strings.Settings_FAQ_URL + if faqUrl == "Settings.FAQ_URL" || faqUrl.isEmpty { + faqUrl = "https://telegram.org/faq#general" + } + let resolvedUrl = resolveInstantViewUrl(account: context.account, url: faqUrl) + + let resolvedUrlPromise = Promise() + resolvedUrlPromise.set(resolvedUrl) + + let openFaq: (Promise) -> Void = { resolvedUrl in + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) + presentControllerImpl?(controller, nil) + let _ = (resolvedUrl.get() + |> take(1) + |> deliverOnMainQueue).start(next: { [weak controller] resolvedUrl in + controller?.dismiss() + dismissImpl?() + + context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, openPeer: { peer, navigation in + }, sendFile: nil, sendSticker: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { controller, arguments in + pushControllerImpl?(controller) + }, dismissInput: {}, contentContext: nil) + }) + } + + presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Settings_FAQ_Intro, actions: [ + TextAlertAction(type: .genericAction, title: presentationData.strings.Settings_FAQ_Button, action: { + openFaq(resolvedUrlPromise) + dismissImpl?() + }), + TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { + supportPeerDisposable.set((supportPeer.get() + |> take(1) + |> deliverOnMainQueue).start(next: { peerId in + if let peerId = peerId, let navigationController = navigationController { + dismissImpl?() + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(id: peerId))) + } + })) + }) + ]), nil) + }, deleteAccount: { + let controller = deleteAccountDataController(context: context, mode: .peers) + replaceTopControllerImpl?(controller, true) + }) + + let signal = combineLatest(queue: .mainQueue(), + context.sharedContext.presentationData, + context.sharedContext.accountManager.accessChallengeData(), + activeAccountsAndPeers(context: context) + ) + |> map { presentationData, accessChallengeData, accountsAndPeers -> (ItemListControllerState, (ItemListNodeState, Any)) in + let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { + dismissImpl?() + }) + + var hasPasscode = false + switch accessChallengeData.data { + case .numericalPassword, .plaintextPassword: + hasPasscode = true + default: + break + } + + let canAddAccounts = accountsAndPeers.1.count + 1 < maximumNumberOfAccounts + + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.DeleteAccount_AlternativeOptionsTitle), leftNavigationButton: leftNavigationButton, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: deleteAccountOptionsEntries(presentationData: presentationData, canAddAccounts: canAddAccounts, hasTwoStepAuth: hasTwoStepAuth, hasPasscode: hasPasscode), style: .blocks) + + return (controllerState, (listState, arguments)) + } + + let controller = ItemListController(context: context, state: signal, tabBarItem: nil) + controller.navigationPresentation = .modal + pushControllerImpl = { [weak navigationController] value in + navigationController?.pushViewController(value, animated: false) + } + presentControllerImpl = { [weak controller] value, arguments in + controller?.present(value, in: .window(.root), with: arguments ?? ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + } + replaceTopControllerImpl = { [weak navigationController] c, complex in + if complex { + navigationController?.pushViewController(c, completion: { [weak navigationController, weak controller, weak c] in + if let navigationController = navigationController { + let controllers = navigationController.viewControllers.filter { $0 !== controller } + c?.navigationPresentation = .modal + navigationController.setViewControllers(controllers, animated: false) + } + }) + } else { + navigationController?.replaceTopController(c, animated: true) + } + } + dismissImpl = { [weak controller] in + let _ = controller?.dismiss() + } + + return controller +} + diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift index 3b8b55c925..6731dbcd62 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift @@ -90,5 +90,10 @@ public struct PresentationResourcesSettings { public static let clearCache = renderIcon(name: "Settings/Menu/ClearCache") public static let changePhoneNumber = renderIcon(name: "Settings/Menu/ChangePhoneNumber") + public static let deleteAddAccount = renderIcon(name: "Settings/Menu/DeleteAddAccount") + public static let deleteSetPasscode = renderIcon(name: "Settings/Menu/FaceId") + public static let deleteChats = renderIcon(name: "Settings/Menu/DeleteChats") + public static let clearSynced = renderIcon(name: "Settings/Menu/ClearSynced") + public static let websites = renderIcon(name: "Settings/Menu/Websites") } diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Gift.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Gift.imageset/Contents.json new file mode 100644 index 0000000000..f933ba9c44 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Gift.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "gift.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Gift.imageset/gift.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Gift.imageset/gift.pdf new file mode 100644 index 0000000000..407ead689d --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Gift.imageset/gift.pdf @@ -0,0 +1,512 @@ +%PDF-1.7 + +1 0 obj + << /Type /XObject + /Length 2 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 24.000000 24.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 7.992188 7.880981 cm +0.000000 0.000000 0.000000 scn +3.800998 1.276080 m +1.993796 0.168980 l +1.805881 0.053862 1.560225 0.112875 1.445107 0.300790 c +1.388883 0.392569 1.372121 0.503168 1.398624 0.607484 c +1.678378 1.708605 l +1.779364 2.106091 2.051364 2.438358 2.421074 2.615862 c +4.392641 3.562446 l +4.484557 3.606576 4.523294 3.716863 4.479164 3.808778 c +4.443426 3.883215 4.362605 3.924868 4.281243 3.910783 c +2.086636 3.530841 l +1.640524 3.453608 1.183040 3.576797 0.836031 3.867601 c +0.142737 4.448601 l +-0.026168 4.590147 -0.048346 4.841818 0.093201 5.010722 c +0.162044 5.092872 0.261042 5.143870 0.367897 5.152233 c +2.486118 5.318005 l +2.635765 5.329716 2.766179 5.424419 2.823627 5.563095 c +3.640796 7.535693 l +3.725137 7.739288 3.958555 7.835961 4.162150 7.751620 c +4.259909 7.711123 4.337579 7.633452 4.378077 7.535693 c +5.195247 5.563095 l +5.252695 5.424419 5.383109 5.329716 5.532755 5.318005 c +7.662615 5.151322 l +7.882316 5.134129 8.046480 4.942087 8.029286 4.722386 c +8.021015 4.616698 7.971026 4.518645 7.890352 4.449868 c +6.265997 3.065068 l +6.151649 2.967583 6.101762 2.814124 6.136929 2.668034 c +6.636304 0.593517 l +6.687879 0.379265 6.556002 0.163769 6.341750 0.112194 c +6.238800 0.087412 6.130221 0.104568 6.039927 0.159883 c +4.217875 1.276080 l +4.089963 1.354439 3.928910 1.354439 3.800998 1.276080 c +h +f* +n +Q + +endstream +endobj + +2 0 obj + 1398 +endobj + +3 0 obj + << /Type /XObject + /Length 4 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 24.000000 24.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 6.000000 5.500000 cm +0.000000 0.000000 0.000000 scn +0.000000 6.000000 m +0.000000 9.313708 2.686292 12.000000 6.000000 12.000000 c +6.000000 12.000000 l +9.313708 12.000000 12.000000 9.313708 12.000000 6.000000 c +12.000000 6.000000 l +12.000000 2.686292 9.313708 0.000000 6.000000 0.000000 c +6.000000 0.000000 l +2.686292 0.000000 0.000000 2.686292 0.000000 6.000000 c +0.000000 6.000000 l +h +f +n +Q + +endstream +endobj + +4 0 obj + 459 +endobj + +5 0 obj + << /XObject << /X1 1 0 R >> + /ExtGState << /E1 << /SMask << /Type /Mask + /G 3 0 R + /S /Alpha + >> + /Type /ExtGState + >> >> + >> +endobj + +6 0 obj + << /Length 7 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 5.000000 2.669434 cm +0.000000 0.000000 0.000000 scn +0.665000 9.330566 m +0.665000 9.697836 0.367269 9.995566 0.000000 9.995566 c +-0.367269 9.995566 -0.665000 9.697836 -0.665000 9.330566 c +0.665000 9.330566 l +h +14.665000 9.330566 m +14.665000 9.697836 14.367270 9.995566 14.000000 9.995566 c +13.632730 9.995566 13.335000 9.697836 13.335000 9.330566 c +14.665000 9.330566 l +h +1.092019 1.548553 m +1.393923 2.141073 l +1.092019 1.548553 l +h +12.907981 1.548553 m +12.606077 2.141073 l +12.907981 1.548553 l +h +13.782013 2.422585 m +13.189494 2.724489 l +13.782013 2.422585 l +h +10.800000 1.995566 m +3.200000 1.995566 l +3.200000 0.665566 l +10.800000 0.665566 l +10.800000 1.995566 l +h +0.665000 4.530566 m +0.665000 9.330566 l +-0.665000 9.330566 l +-0.665000 4.530566 l +0.665000 4.530566 l +h +13.335000 9.330566 m +13.335000 4.530566 l +14.665000 4.530566 l +14.665000 9.330566 l +13.335000 9.330566 l +h +3.200000 1.995566 m +2.628974 1.995566 2.240699 1.996084 1.940556 2.020606 c +1.648176 2.044495 1.498463 2.087807 1.393923 2.141073 c +0.790115 0.956034 l +1.113398 0.791313 1.457623 0.725632 1.832252 0.695024 c +2.199117 0.665050 2.650920 0.665566 3.200000 0.665566 c +3.200000 1.995566 l +h +-0.665000 4.530566 m +-0.665000 3.981487 -0.665517 3.529683 -0.635543 3.162818 c +-0.604935 2.788190 -0.539253 2.443964 -0.374532 2.120682 c +0.810506 2.724489 l +0.757240 2.829030 0.713928 2.978743 0.690040 3.271122 c +0.665517 3.571266 0.665000 3.959541 0.665000 4.530566 c +-0.665000 4.530566 l +h +1.393923 2.141073 m +1.142726 2.269063 0.938497 2.473293 0.810506 2.724489 c +-0.374532 2.120682 l +-0.119030 1.619230 0.288663 1.211536 0.790115 0.956034 c +1.393923 2.141073 l +h +10.800000 0.665566 m +11.349079 0.665566 11.800883 0.665050 12.167748 0.695024 c +12.542377 0.725632 12.886601 0.791313 13.209885 0.956034 c +12.606077 2.141073 l +12.501536 2.087807 12.351824 2.044495 12.059443 2.020606 c +11.759300 1.996084 11.371026 1.995566 10.800000 1.995566 c +10.800000 0.665566 l +h +13.335000 4.530566 m +13.335000 3.959541 13.334483 3.571266 13.309960 3.271122 c +13.286072 2.978743 13.242760 2.829030 13.189494 2.724489 c +14.374533 2.120682 l +14.539253 2.443964 14.604935 2.788189 14.635543 3.162818 c +14.665517 3.529683 14.665000 3.981487 14.665000 4.530566 c +13.335000 4.530566 l +h +13.209885 0.956034 m +13.711337 1.211536 14.119030 1.619230 14.374533 2.120682 c +13.189494 2.724489 l +13.061502 2.473293 12.857274 2.269063 12.606077 2.141073 c +13.209885 0.956034 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 4.000000 11.169189 cm +0.000000 0.000000 0.000000 scn +2.000000 0.665811 m +2.367269 0.665811 2.665000 0.963541 2.665000 1.330811 c +2.665000 1.698080 2.367269 1.995811 2.000000 1.995811 c +2.000000 0.665811 l +h +14.000000 1.995811 m +13.632730 1.995811 13.335000 1.698080 13.335000 1.330811 c +13.335000 0.963541 13.632730 0.665811 14.000000 0.665811 c +14.000000 1.995811 l +h +0.115213 2.017745 m +0.716366 2.302069 l +0.115213 2.017745 l +h +0.686934 1.446023 m +0.402610 0.844871 l +0.686934 1.446023 l +h +15.884788 2.017745 m +16.485941 1.733420 l +15.884788 2.017745 l +h +15.313066 1.446023 m +15.597390 0.844871 l +15.313066 1.446023 l +h +14.855110 6.138789 m +15.139435 6.739942 l +14.855110 6.138789 l +h +15.807979 5.185921 m +15.206825 4.901597 l +15.807979 5.185921 l +h +3.125000 5.665811 m +12.875000 5.665811 l +12.875000 6.995811 l +3.125000 6.995811 l +3.125000 5.665811 l +h +1.875000 0.665811 m +2.000000 0.665811 l +2.000000 1.995811 l +1.875000 1.995811 l +1.875000 0.665811 l +h +14.125000 1.995811 m +14.000000 1.995811 l +14.000000 0.665811 l +14.125000 0.665811 l +14.125000 1.995811 l +h +-0.665000 3.205811 m +-0.665000 2.901255 -0.665455 2.635354 -0.648653 2.416179 c +-0.631310 2.189949 -0.592755 1.959262 -0.485940 1.733420 c +0.716366 2.302069 l +0.707968 2.319824 0.688916 2.368348 0.677456 2.517840 c +0.665455 2.674387 0.665000 2.880721 0.665000 3.205811 c +-0.665000 3.205811 l +h +1.875000 1.995811 m +1.549910 1.995811 1.343576 1.996265 1.187029 2.008266 c +1.037537 2.019727 0.989013 2.038779 0.971258 2.047176 c +0.402610 0.844871 l +0.628452 0.738055 0.859138 0.699501 1.085368 0.682158 c +1.304543 0.665356 1.570444 0.665811 1.875000 0.665811 c +1.875000 1.995811 l +h +-0.485940 1.733420 m +-0.301460 1.343369 0.012559 1.029351 0.402610 0.844871 c +0.971258 2.047176 l +0.859367 2.100097 0.769286 2.190177 0.716366 2.302069 c +-0.485940 1.733420 l +h +15.335000 3.205811 m +15.335000 2.880721 15.334545 2.674387 15.322544 2.517839 c +15.311084 2.368348 15.292032 2.319824 15.283634 2.302069 c +16.485941 1.733420 l +16.592754 1.959262 16.631310 2.189949 16.648653 2.416179 c +16.665455 2.635354 16.665001 2.901255 16.665001 3.205811 c +15.335000 3.205811 l +h +14.125000 0.665811 m +14.429556 0.665811 14.695457 0.665356 14.914632 0.682158 c +15.140862 0.699501 15.371549 0.738055 15.597390 0.844871 c +15.028742 2.047176 l +15.010986 2.038779 14.962463 2.019727 14.812971 2.008266 c +14.656424 1.996265 14.450090 1.995811 14.125000 1.995811 c +14.125000 0.665811 l +h +15.283634 2.302069 m +15.230714 2.190177 15.140634 2.100097 15.028742 2.047176 c +15.597390 0.844871 l +15.987441 1.029351 16.301460 1.343369 16.485941 1.733420 c +15.283634 2.302069 l +h +12.875000 5.665811 m +13.409972 5.665811 13.773717 5.665356 14.055506 5.643754 c +14.330238 5.622692 14.471831 5.584438 14.570786 5.537636 c +15.139435 6.739942 l +14.832394 6.885161 14.508637 6.942918 14.157166 6.969862 c +13.812751 6.996265 13.389438 6.995811 12.875000 6.995811 c +12.875000 5.665811 l +h +16.665001 3.205811 m +16.665001 3.720248 16.665455 4.143561 16.639051 4.487977 c +16.612108 4.839448 16.554352 5.163204 16.409132 5.470245 c +15.206825 4.901597 l +15.253628 4.802642 15.291882 4.661048 15.312943 4.386316 c +15.334545 4.104527 15.335000 3.740782 15.335000 3.205811 c +16.665001 3.205811 l +h +14.570786 5.537636 m +14.849992 5.405582 15.074771 5.180802 15.206825 4.901597 c +16.409132 5.470245 l +16.145517 6.027610 15.696799 6.476328 15.139435 6.739942 c +14.570786 5.537636 l +h +3.125000 6.995811 m +2.610562 6.995811 2.187250 6.996265 1.842834 6.969862 c +1.491363 6.942918 1.167606 6.885161 0.860566 6.739942 c +1.429214 5.537636 l +1.528168 5.584438 1.669762 5.622692 1.944495 5.643754 c +2.226283 5.665356 2.590028 5.665811 3.125000 5.665811 c +3.125000 6.995811 l +h +0.665000 3.205811 m +0.665000 3.740783 0.665455 4.104527 0.687057 4.386316 c +0.708118 4.661048 0.746372 4.802642 0.793174 4.901597 c +-0.409131 5.470245 l +-0.554351 5.163204 -0.612108 4.839448 -0.639052 4.487977 c +-0.665455 4.143561 -0.665000 3.720249 -0.665000 3.205811 c +0.665000 3.205811 l +h +0.860566 6.739942 m +0.303200 6.476328 -0.145517 6.027610 -0.409131 5.470245 c +0.793174 4.901597 l +0.925229 5.180802 1.150008 5.405582 1.429214 5.537636 c +0.860566 6.739942 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 8.000000 16.169922 cm +0.000000 0.000000 0.000000 scn +4.000000 1.330078 m +4.000000 0.665078 l +4.665000 0.665078 l +4.665000 1.330078 l +4.000000 1.330078 l +h +3.335000 3.330078 m +3.335000 1.330078 l +4.665000 1.330078 l +4.665000 3.330078 l +3.335000 3.330078 l +h +4.000000 1.995078 m +2.000000 1.995078 l +2.000000 0.665078 l +4.000000 0.665078 l +4.000000 1.995078 l +h +2.000000 1.995078 m +1.262700 1.995078 0.665000 2.592778 0.665000 3.330078 c +-0.665000 3.330078 l +-0.665000 1.858239 0.528161 0.665078 2.000000 0.665078 c +2.000000 1.995078 l +h +2.000000 4.665078 m +2.737300 4.665078 3.335000 4.067378 3.335000 3.330078 c +4.665000 3.330078 l +4.665000 4.801917 3.471839 5.995078 2.000000 5.995078 c +2.000000 4.665078 l +h +2.000000 5.995078 m +0.528161 5.995078 -0.665000 4.801917 -0.665000 3.330078 c +0.665000 3.330078 l +0.665000 4.067378 1.262700 4.665078 2.000000 4.665078 c +2.000000 5.995078 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 12.000000 16.169922 cm +0.000000 0.000000 0.000000 scn +0.000000 1.330078 m +-0.665000 1.330078 l +-0.665000 0.665078 l +0.000000 0.665078 l +0.000000 1.330078 l +h +2.000000 1.995078 m +0.000000 1.995078 l +0.000000 0.665078 l +2.000000 0.665078 l +2.000000 1.995078 l +h +0.665000 1.330078 m +0.665000 3.330078 l +-0.665000 3.330078 l +-0.665000 1.330078 l +0.665000 1.330078 l +h +3.335000 3.330078 m +3.335000 2.592778 2.737300 1.995078 2.000000 1.995078 c +2.000000 0.665078 l +3.471839 0.665078 4.665000 1.858239 4.665000 3.330078 c +3.335000 3.330078 l +h +2.000000 4.665078 m +2.737300 4.665078 3.335000 4.067378 3.335000 3.330078 c +4.665000 3.330078 l +4.665000 4.801917 3.471839 5.995078 2.000000 5.995078 c +2.000000 4.665078 l +h +2.000000 5.995078 m +0.528161 5.995078 -0.665000 4.801917 -0.665000 3.330078 c +0.665000 3.330078 l +0.665000 4.067378 1.262700 4.665078 2.000000 4.665078 c +2.000000 5.995078 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 12.000000 3.169922 cm +0.000000 0.000000 0.000000 scn +0.665000 3.330078 m +0.665000 3.697347 0.367269 3.995078 0.000000 3.995078 c +-0.367269 3.995078 -0.665000 3.697347 -0.665000 3.330078 c +0.665000 3.330078 l +h +-0.665000 1.330078 m +-0.665000 0.962809 -0.367269 0.665078 0.000000 0.665078 c +0.367269 0.665078 0.665000 0.962809 0.665000 1.330078 c +-0.665000 1.330078 l +h +-0.665000 3.330078 m +-0.665000 1.330078 l +0.665000 1.330078 l +0.665000 3.330078 l +-0.665000 3.330078 l +h +f +n +Q +q +/E1 gs +/X1 Do +Q + +endstream +endobj + +7 0 obj + 9085 +endobj + +8 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ] + /Resources 5 0 R + /Contents 6 0 R + /Parent 9 0 R + >> +endobj + +9 0 obj + << /Kids [ 8 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +10 0 obj + << /Pages 9 0 R + /Type /Catalog + >> +endobj + +xref +0 11 +0000000000 65535 f +0000000010 00000 n +0000001656 00000 n +0000001679 00000 n +0000002386 00000 n +0000002408 00000 n +0000002706 00000 n +0000011847 00000 n +0000011870 00000 n +0000012043 00000 n +0000012117 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 10 0 R + /Size 11 +>> +startxref +12177 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Menu/ClearSynced.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/Menu/ClearSynced.imageset/Contents.json new file mode 100644 index 0000000000..210f015b9f --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/Menu/ClearSynced.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "clearsynced.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Menu/ClearSynced.imageset/clearsynced.pdf b/submodules/TelegramUI/Images.xcassets/Settings/Menu/ClearSynced.imageset/clearsynced.pdf new file mode 100644 index 0000000000..80e9499bc2 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/Menu/ClearSynced.imageset/clearsynced.pdf @@ -0,0 +1,136 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm +1.000000 0.584314 0.000000 scn +0.000000 18.799999 m +0.000000 22.720367 0.000000 24.680552 0.762954 26.177933 c +1.434068 27.495068 2.504932 28.565931 3.822066 29.237045 c +5.319448 30.000000 7.279633 30.000000 11.200000 30.000000 c +18.799999 30.000000 l +22.720367 30.000000 24.680552 30.000000 26.177933 29.237045 c +27.495068 28.565931 28.565931 27.495068 29.237045 26.177933 c +30.000000 24.680552 30.000000 22.720367 30.000000 18.799999 c +30.000000 11.200001 l +30.000000 7.279633 30.000000 5.319448 29.237045 3.822067 c +28.565931 2.504932 27.495068 1.434069 26.177933 0.762955 c +24.680552 0.000000 22.720367 0.000000 18.799999 0.000000 c +11.200000 0.000000 l +7.279633 0.000000 5.319448 0.000000 3.822066 0.762955 c +2.504932 1.434069 1.434068 2.504932 0.762954 3.822067 c +0.000000 5.319448 0.000000 7.279633 0.000000 11.200001 c +0.000000 18.799999 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 2.652100 8.000000 cm +1.000000 1.000000 1.000000 scn +15.347914 11.500000 m +15.347914 9.843145 14.004768 8.500000 12.347914 8.500000 c +10.691060 8.500000 9.347914 9.843145 9.347914 11.500000 c +9.347914 13.156855 10.691060 14.500000 12.347914 14.500000 c +14.004768 14.500000 15.347914 13.156855 15.347914 11.500000 c +h +16.206648 5.096860 m +17.131365 4.487454 17.732334 3.680024 18.122902 2.887923 c +18.464052 2.196046 18.353537 1.522406 17.970762 1.000000 c +17.531406 0.400373 16.733355 -0.000002 15.847914 -0.000002 c +8.847914 -0.000002 l +7.962472 -0.000002 7.164421 0.400373 6.725066 1.000000 c +6.701973 1.031517 6.679871 1.063584 6.658800 1.096172 c +6.330600 1.603753 6.252357 2.237787 6.572926 2.887924 c +6.987937 3.729597 7.640509 4.588578 8.666627 5.208897 c +9.571874 5.756145 10.767847 6.117645 12.347914 6.117645 c +13.927979 6.117645 15.123951 5.756145 16.029198 5.208899 c +16.089634 5.172363 16.148775 5.134999 16.206648 5.096860 c +h +5.347910 6.000000 m +6.038868 6.000000 6.650654 5.923974 7.192287 5.790671 c +6.346507 5.094717 5.770174 4.267296 5.380054 3.476105 c +4.957832 2.619810 4.950275 1.751801 5.228493 1.000000 c +2.054037 1.000000 l +0.595407 1.000000 -0.530585 2.298071 0.261390 3.522970 c +1.064612 4.765267 2.563349 6.000000 5.347910 6.000000 c +h +19.315775 3.476104 m +19.737995 2.619809 19.745552 1.751801 19.467335 1.000000 c +22.641785 1.000000 l +24.100414 1.000000 25.226404 2.298071 24.434431 3.522971 c +23.631208 4.765267 22.132471 6.000000 19.347910 6.000000 c +18.656954 6.000000 18.045172 5.923975 17.503540 5.790672 c +18.349321 5.094717 18.925655 4.267296 19.315775 3.476104 c +h +7.847914 10.500000 m +7.847914 9.119288 6.728626 8.000000 5.347914 8.000000 c +3.967202 8.000000 2.847914 9.119288 2.847914 10.500000 c +2.847914 11.880713 3.967202 13.000000 5.347914 13.000000 c +6.728626 13.000000 7.847914 11.880713 7.847914 10.500000 c +h +21.847914 10.500000 m +21.847914 9.119288 20.728626 8.000000 19.347914 8.000000 c +17.967201 8.000000 16.847914 9.119288 16.847914 10.500000 c +16.847914 11.880713 17.967201 13.000000 19.347914 13.000000 c +20.728626 13.000000 21.847914 11.880713 21.847914 10.500000 c +h +f* +n +Q + +endstream +endobj + +3 0 obj + 3113 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000003203 00000 n +0000003226 00000 n +0000003399 00000 n +0000003473 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +3532 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Menu/DeleteAddAccount.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/Menu/DeleteAddAccount.imageset/Contents.json new file mode 100644 index 0000000000..bd652ccbdd --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/Menu/DeleteAddAccount.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "addaccount.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Menu/DeleteAddAccount.imageset/addaccount.pdf b/submodules/TelegramUI/Images.xcassets/Settings/Menu/DeleteAddAccount.imageset/addaccount.pdf new file mode 100644 index 0000000000..af8d877527 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/Menu/DeleteAddAccount.imageset/addaccount.pdf @@ -0,0 +1,102 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm +1.000000 0.584314 0.000000 scn +0.000000 18.799999 m +0.000000 22.720367 0.000000 24.680552 0.762954 26.177933 c +1.434068 27.495068 2.504932 28.565931 3.822066 29.237045 c +5.319448 30.000000 7.279633 30.000000 11.200000 30.000000 c +18.799999 30.000000 l +22.720367 30.000000 24.680552 30.000000 26.177933 29.237045 c +27.495068 28.565931 28.565931 27.495068 29.237045 26.177933 c +30.000000 24.680552 30.000000 22.720367 30.000000 18.799999 c +30.000000 11.200001 l +30.000000 7.279633 30.000000 5.319448 29.237045 3.822067 c +28.565931 2.504932 27.495068 1.434069 26.177933 0.762955 c +24.680552 0.000000 22.720367 0.000000 18.799999 0.000000 c +11.200000 0.000000 l +7.279633 0.000000 5.319448 0.000000 3.822066 0.762955 c +2.504932 1.434069 1.434068 2.504932 0.762954 3.822067 c +0.000000 5.319448 0.000000 7.279633 0.000000 11.200001 c +0.000000 18.799999 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 6.730286 6.000000 cm +1.000000 1.000000 1.000000 scn +8.269731 10.500000 m +10.478869 10.500000 12.269731 12.290861 12.269731 14.500000 c +12.269731 16.709139 10.478869 18.500000 8.269731 18.500000 c +6.060592 18.500000 4.269731 16.709139 4.269731 14.500000 c +4.269731 12.290861 6.060592 10.500000 8.269731 10.500000 c +h +16.238016 3.829396 m +15.145898 5.884616 12.897719 8.000000 8.269734 8.000000 c +3.641746 8.000000 1.393565 5.884617 0.301445 3.829397 c +-0.735194 1.878584 1.060591 0.000000 3.269730 0.000000 c +13.269731 0.000000 l +15.478869 0.000000 17.274656 1.878582 16.238016 3.829396 c +h +f* +n +Q + +endstream +endobj + +3 0 obj + 1580 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000001670 00000 n +0000001693 00000 n +0000001866 00000 n +0000001940 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1999 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Menu/DeleteChats.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/Menu/DeleteChats.imageset/Contents.json new file mode 100644 index 0000000000..561d5e7e17 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/Menu/DeleteChats.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "deletechats.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Menu/DeleteChats.imageset/deletechats.pdf b/submodules/TelegramUI/Images.xcassets/Settings/Menu/DeleteChats.imageset/deletechats.pdf new file mode 100644 index 0000000000..63d5babb07 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/Menu/DeleteChats.imageset/deletechats.pdf @@ -0,0 +1,175 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm +1.000000 0.176471 0.333333 scn +0.000000 18.799999 m +0.000000 22.720367 0.000000 24.680552 0.762954 26.177933 c +1.434068 27.495068 2.504932 28.565931 3.822066 29.237045 c +5.319448 30.000000 7.279633 30.000000 11.200000 30.000000 c +18.799999 30.000000 l +22.720367 30.000000 24.680552 30.000000 26.177933 29.237045 c +27.495068 28.565931 28.565931 27.495068 29.237045 26.177933 c +30.000000 24.680552 30.000000 22.720367 30.000000 18.799999 c +30.000000 11.200001 l +30.000000 7.279633 30.000000 5.319448 29.237045 3.822067 c +28.565931 2.504932 27.495068 1.434069 26.177933 0.762955 c +24.680552 0.000000 22.720367 0.000000 18.799999 0.000000 c +11.200000 0.000000 l +7.279633 0.000000 5.319448 0.000000 3.822066 0.762955 c +2.504932 1.434069 1.434068 2.504932 0.762954 3.822067 c +0.000000 5.319448 0.000000 7.279633 0.000000 11.200001 c +0.000000 18.799999 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 6.834961 5.000000 cm +1.000000 1.000000 1.000000 scn +7.038761 21.165039 m +7.064992 21.165039 l +9.264992 21.165039 l +9.291224 21.165039 l +9.291246 21.165039 l +9.688916 21.165049 10.026937 21.165058 10.304341 21.142393 c +10.595594 21.118597 10.878077 21.066540 11.147882 20.929068 c +11.555252 20.721500 11.886456 20.390299 12.094021 19.982927 c +12.231494 19.713121 12.283551 19.430639 12.307348 19.139387 c +12.330013 18.861979 12.330004 18.523952 12.329992 18.126278 c +12.329992 18.100037 l +12.329992 17.665022 l +15.665000 17.665022 l +16.032269 17.665022 16.330000 17.367292 16.330000 17.000023 c +16.330000 16.632753 16.032269 16.335022 15.665000 16.335022 c +0.665000 16.335022 l +0.297731 16.335022 0.000000 16.632753 0.000000 17.000023 c +0.000000 17.367292 0.297731 17.665022 0.665000 17.665022 c +3.999992 17.665022 l +3.999992 18.100037 l +3.999992 18.126268 l +3.999981 18.523949 3.999972 18.861977 4.022637 19.139387 c +4.046433 19.430639 4.098491 19.713121 4.235963 19.982927 c +4.443529 20.390299 4.774732 20.721500 5.182103 20.929068 c +5.451908 21.066540 5.734391 21.118597 6.025643 21.142393 c +6.303048 21.165058 6.641069 21.165049 7.038738 21.165039 c +7.038761 21.165039 l +h +10.999992 18.100037 m +10.999992 17.665022 l +5.329992 17.665022 l +5.329992 18.100037 l +5.329992 18.531050 5.330510 18.814316 5.348220 19.031082 c +5.365296 19.240086 5.394984 19.328056 5.421002 19.379120 c +5.501056 19.536236 5.628795 19.663975 5.785910 19.744028 c +5.836973 19.770046 5.924943 19.799734 6.133947 19.816811 c +6.350715 19.834520 6.633980 19.835037 7.064992 19.835037 c +9.264992 19.835037 l +9.696005 19.835037 9.979270 19.834520 10.196037 19.816811 c +10.405041 19.799734 10.493011 19.770046 10.544075 19.744028 c +10.701189 19.663975 10.828928 19.536236 10.908983 19.379120 c +10.935000 19.328056 10.964688 19.240086 10.981765 19.031082 c +10.999475 18.814316 10.999992 18.531050 10.999992 18.100037 c +h +1.866287 4.480732 m +1.278763 13.293592 l +1.239206 13.886940 1.219428 14.183613 1.322701 14.412016 c +1.413458 14.612740 1.567953 14.777878 1.762195 14.881785 c +1.983222 15.000023 2.280554 15.000023 2.875219 15.000023 c +13.454782 15.000023 l +14.049447 15.000023 14.346780 15.000023 14.567807 14.881785 c +14.762049 14.777878 14.916544 14.612740 15.007301 14.412016 c +15.110574 14.183613 15.090796 13.886940 15.051239 13.293592 c +14.463715 4.480732 l +14.358499 2.902485 14.305890 2.113361 13.965018 1.515018 c +13.664913 0.988235 13.212244 0.564739 12.666664 0.300339 c +12.046970 0.000023 11.256096 0.000023 9.674346 0.000023 c +6.655656 0.000023 l +5.073906 0.000023 4.283031 0.000023 3.663338 0.300339 c +3.117758 0.564739 2.665089 0.988235 2.364984 1.515018 c +2.024112 2.113361 1.971503 2.902485 1.866287 4.480732 c +h +5.420216 12.030214 m +5.403539 12.397105 5.092596 12.681009 4.725706 12.664333 c +4.358815 12.647655 4.074911 12.336713 4.091588 11.969823 c +4.500678 2.969837 l +4.517354 2.602947 4.828298 2.319042 5.195188 2.335720 c +5.562078 2.352396 5.845983 2.663338 5.829306 3.030230 c +5.420216 12.030214 l +h +11.604276 12.664333 m +11.971167 12.647655 12.255072 12.336713 12.238394 11.969823 c +11.829304 2.969837 l +11.812627 2.602947 11.501684 2.319042 11.134793 2.335720 c +10.767903 2.352396 10.483999 2.663338 10.500675 3.030230 c +10.909766 12.030214 l +10.926443 12.397105 11.237386 12.681009 11.604276 12.664333 c +h +8.164989 12.665019 m +8.532259 12.665019 8.829989 12.367289 8.829989 12.000019 c +8.829989 3.000034 l +8.829989 2.632763 8.532259 2.335033 8.164989 2.335033 c +7.797720 2.335033 7.499990 2.632763 7.499990 3.000034 c +7.499990 12.000019 l +7.499990 12.367289 7.797720 12.665019 8.164989 12.665019 c +h +f* +n +Q + +endstream +endobj + +3 0 obj + 4588 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000004678 00000 n +0000004701 00000 n +0000004874 00000 n +0000004948 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +5007 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/AccountContext.swift b/submodules/TelegramUI/Sources/AccountContext.swift index 944d5bcb33..5303f74f6e 100644 --- a/submodules/TelegramUI/Sources/AccountContext.swift +++ b/submodules/TelegramUI/Sources/AccountContext.swift @@ -186,11 +186,8 @@ public final class AccountContextImpl: AccountContext { self.prefetchManager = PrefetchManagerImpl(sharedContext: sharedContext, account: account, engine: self.engine, fetchManager: self.fetchManager) self.wallpaperUploadManager = WallpaperUploadManagerImpl(sharedContext: sharedContext, account: account, presentationData: sharedContext.presentationData) self.themeUpdateManager = ThemeUpdateManagerImpl(sharedContext: sharedContext, account: account) - if let premiumProductId = sharedContext.premiumProductId { - self.inAppPurchaseManager = InAppPurchaseManager(engine: self.engine, premiumProductId: premiumProductId) - } else { - self.inAppPurchaseManager = nil - } + + self.inAppPurchaseManager = InAppPurchaseManager(engine: self.engine) } else { self.prefetchManager = nil self.wallpaperUploadManager = nil diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index e8bef330e6..a4e7eb3681 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -773,7 +773,7 @@ private func extractAccountManagerState(records: AccountRecordsView Void)? - let sharedContext = SharedAccountContextImpl(mainWindow: self.mainWindow, sharedContainerPath: legacyBasePath, basePath: rootPath, encryptionParameters: encryptionParameters, accountManager: accountManager, appLockContext: appLockContext, applicationBindings: applicationBindings, initialPresentationDataAndSettings: initialPresentationDataAndSettings, networkArguments: networkArguments, premiumProductId: buildConfig.premiumIAPProductId, rootPath: rootPath, legacyBasePath: legacyBasePath, apsNotificationToken: self.notificationTokenPromise.get() |> map(Optional.init), voipNotificationToken: self.voipTokenPromise.get() |> map(Optional.init), setNotificationCall: { call in + let sharedContext = SharedAccountContextImpl(mainWindow: self.mainWindow, sharedContainerPath: legacyBasePath, basePath: rootPath, encryptionParameters: encryptionParameters, accountManager: accountManager, appLockContext: appLockContext, applicationBindings: applicationBindings, initialPresentationDataAndSettings: initialPresentationDataAndSettings, networkArguments: networkArguments, hasInAppPurchases: buildConfig.isAppStoreBuild && buildConfig.apiId == 1, rootPath: rootPath, legacyBasePath: legacyBasePath, apsNotificationToken: self.notificationTokenPromise.get() |> map(Optional.init), voipNotificationToken: self.voipTokenPromise.get() |> map(Optional.init), setNotificationCall: { call in setPresentationCall?(call) }, navigateToChat: { accountId, peerId, messageId in self.openChatWhenReady(accountId: accountId, peerId: peerId, messageId: messageId) diff --git a/submodules/TelegramUI/Sources/ChatMessageDateHeader.swift b/submodules/TelegramUI/Sources/ChatMessageDateHeader.swift index 52b374d55e..790a8655da 100644 --- a/submodules/TelegramUI/Sources/ChatMessageDateHeader.swift +++ b/submodules/TelegramUI/Sources/ChatMessageDateHeader.swift @@ -395,6 +395,7 @@ final class ChatMessageAvatarHeaderNode: ListViewItemHeaderNode { private let containerNode: ContextControllerSourceNode private let avatarNode: AvatarNode private var videoNode: UniversalVideoNode? + private var credibilityIconNode: ASImageNode? private var videoContent: NativeVideoContent? private let playbackStartDisposable = MetaDisposable() @@ -519,7 +520,22 @@ final class ChatMessageAvatarHeaderNode: ListViewItemHeaderNode { let _ = context.engine.peers.fetchAndUpdateCachedPeerData(peerId: peer.id).start() } })) + + let credibilityIconNode: ASImageNode + if let current = self.credibilityIconNode { + credibilityIconNode = current + } else { + credibilityIconNode = ASImageNode() + credibilityIconNode.displaysAsynchronously = false + credibilityIconNode.displayWithoutProcessing = true + credibilityIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat List/PeerPremiumIcon"), color: .white) + self.containerNode.addSubnode(credibilityIconNode) + } + credibilityIconNode.frame = CGRect(origin: CGPoint(x: 29.0 - UIScreenPixel, y: 29.0 - UIScreenPixel), size: CGSize(width: 10.0, height: 10.0)) } else { + self.credibilityIconNode?.removeFromSupernode() + self.credibilityIconNode = nil + self.cachedDataDisposable.set(nil) self.videoContent = nil diff --git a/submodules/TelegramUI/Sources/NotificationContentContext.swift b/submodules/TelegramUI/Sources/NotificationContentContext.swift index a74a244e63..035a2cf1cf 100644 --- a/submodules/TelegramUI/Sources/NotificationContentContext.swift +++ b/submodules/TelegramUI/Sources/NotificationContentContext.swift @@ -138,7 +138,7 @@ public final class NotificationViewControllerImpl { return nil }) - sharedAccountContext = SharedAccountContextImpl(mainWindow: nil, sharedContainerPath: self.initializationData.appGroupPath, basePath: rootPath, encryptionParameters: ValueBoxEncryptionParameters(forceEncryptionIfNoSet: false, key: ValueBoxEncryptionParameters.Key(data: self.initializationData.encryptionParameters.0)!, salt: ValueBoxEncryptionParameters.Salt(data: self.initializationData.encryptionParameters.1)!), accountManager: accountManager, appLockContext: appLockContext, applicationBindings: applicationBindings, initialPresentationDataAndSettings: initialPresentationDataAndSettings!, networkArguments: NetworkInitializationArguments(apiId: self.initializationData.apiId, apiHash: self.initializationData.apiHash, languagesCategory: self.initializationData.languagesCategory, appVersion: self.initializationData.appVersion, voipMaxLayer: 0, voipVersions: [], appData: .single(self.initializationData.bundleData), autolockDeadine: .single(nil), encryptionProvider: OpenSSLEncryptionProvider(), resolvedDeviceName: nil), premiumProductId: nil, rootPath: rootPath, legacyBasePath: nil, apsNotificationToken: .never(), voipNotificationToken: .never(), setNotificationCall: { _ in }, navigateToChat: { _, _, _ in }) + sharedAccountContext = SharedAccountContextImpl(mainWindow: nil, sharedContainerPath: self.initializationData.appGroupPath, basePath: rootPath, encryptionParameters: ValueBoxEncryptionParameters(forceEncryptionIfNoSet: false, key: ValueBoxEncryptionParameters.Key(data: self.initializationData.encryptionParameters.0)!, salt: ValueBoxEncryptionParameters.Salt(data: self.initializationData.encryptionParameters.1)!), accountManager: accountManager, appLockContext: appLockContext, applicationBindings: applicationBindings, initialPresentationDataAndSettings: initialPresentationDataAndSettings!, networkArguments: NetworkInitializationArguments(apiId: self.initializationData.apiId, apiHash: self.initializationData.apiHash, languagesCategory: self.initializationData.languagesCategory, appVersion: self.initializationData.appVersion, voipMaxLayer: 0, voipVersions: [], appData: .single(self.initializationData.bundleData), autolockDeadine: .single(nil), encryptionProvider: OpenSSLEncryptionProvider(), resolvedDeviceName: nil), hasInAppPurchases: false, rootPath: rootPath, legacyBasePath: nil, apsNotificationToken: .never(), voipNotificationToken: .never(), setNotificationCall: { _ in }, navigateToChat: { _, _, _ in }) presentationDataPromise.set(sharedAccountContext!.presentationData) } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 7b34ad0bcb..9b550ad332 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -3874,11 +3874,6 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate let filteredButtons = allHeaderButtons.subtracting(headerButtons) - var canChangeColors = false - if let peer = peer as? TelegramUser, peer.botInfo == nil && strongSelf.data?.encryptionKeyFingerprint == nil { - canChangeColors = true - } - var currentAutoremoveTimeout: Int32? if let cachedData = data.cachedData as? CachedUserData { switch cachedData.autoremoveTimeout { @@ -3925,77 +3920,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate canSetupAutoremoveTimeout = true } } - - if canSetupAutoremoveTimeout { - let strings = strongSelf.presentationData.strings - items.append(.action(ContextMenuActionItem(text: currentAutoremoveTimeout == nil ? strongSelf.presentationData.strings.PeerInfo_EnableAutoDelete : strongSelf.presentationData.strings.PeerInfo_AdjustAutoDelete, icon: { theme in - if let currentAutoremoveTimeout = currentAutoremoveTimeout { - let text = NSAttributedString(string: shortTimeIntervalString(strings: strings, value: currentAutoremoveTimeout), font: Font.regular(14.0), textColor: theme.contextMenu.primaryColor) - let bounds = text.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil) - return generateImage(bounds.size.integralFloor, rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - UIGraphicsPushContext(context) - text.draw(in: bounds) - UIGraphicsPopContext() - }) - } else { - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Timer"), color: theme.contextMenu.primaryColor) - } - }, action: { [weak self] c, _ in - var subItems: [ContextMenuItem] = [] - - subItems.append(.action(ContextMenuActionItem(text: strings.Common_Back, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor) - }, action: { c, _ in - c.popItems() - }))) - subItems.append(.separator) - - let presetValues: [Int32] = [ - 1 * 24 * 60 * 60, - 7 * 24 * 60 * 60, - 31 * 24 * 60 * 60 - ] - - for value in presetValues { - subItems.append(.action(ContextMenuActionItem(text: timeIntervalString(strings: strings, value: value), icon: { _ in - return nil - }, action: { _, f in - f(.default) - - self?.setAutoremove(timeInterval: value) - }))) - } - - subItems.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.PeerInfo_AutoDeleteSettingOther, icon: { _ in - return nil - }, action: { _, f in - f(.default) - self?.openAutoremove(currentValue: currentAutoremoveTimeout) - }))) - - if let _ = currentAutoremoveTimeout { - subItems.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.PeerInfo_AutoDeleteDisable, textColor: .destructive, icon: { _ in - return nil - }, action: { _, f in - f(.default) - - self?.setAutoremove(timeInterval: nil) - }))) - } - - subItems.append(.separator) - - subItems.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.PeerInfo_AutoDeleteInfo, textLayout: .multiline, textFont: .small, icon: { _ in - return nil - }, action: nil as ((ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void) -> Void)?))) - - c.pushItems(items: .single(ContextController.Items(content: .list(subItems)))) - }))) - items.append(.separator) - } - if filteredButtons.contains(.call) { items.append(.action(ContextMenuActionItem(text: presentationData.strings.PeerInfo_ButtonCall, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Call"), color: theme.contextMenu.primaryColor) @@ -4014,6 +3939,16 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate } if let user = peer as? TelegramUser { + if user.botInfo == nil && strongSelf.data?.encryptionKeyFingerprint == nil { + items.append(.action(ContextMenuActionItem(text: presentationData.strings.UserInfo_ChangeColors, icon: { theme in + generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ApplyTheme"), color: theme.contextMenu.primaryColor) + }, action: { _, f in + f(.dismissWithoutContent) + + self?.openChatForThemeChange() + }))) + } + if let _ = user.botInfo { if user.username != nil { items.append(.action(ContextMenuActionItem(text: presentationData.strings.UserInfo_ShareBot, icon: { theme in @@ -4052,6 +3987,16 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate } } + if strongSelf.peerId.namespace == Namespaces.Peer.CloudUser && user.botInfo == nil && !user.flags.contains(.isSupport) { + items.append(.action(ContextMenuActionItem(text: presentationData.strings.UserInfo_StartSecretChat, icon: { theme in + generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Lock"), color: theme.contextMenu.primaryColor) + }, action: { _, f in + f(.dismissWithoutContent) + + self?.openStartSecretChat() + }))) + } + if user.botInfo == nil && data.isContact { items.append(.action(ContextMenuActionItem(text: presentationData.strings.Profile_ShareContactButton, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) @@ -4107,23 +4052,83 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate }))) } - if canChangeColors { - items.append(.action(ContextMenuActionItem(text: presentationData.strings.UserInfo_ChangeColors, icon: { theme in - generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ApplyTheme"), color: theme.contextMenu.primaryColor) - }, action: { _, f in + if !user.isDeleted && user.botInfo == nil && !user.flags.contains(.isSupport) { + items.append(.action(ContextMenuActionItem(text: presentationData.strings.PeerInfo_GiftPremium, icon: { theme in + generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Gift"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, f in f(.dismissWithoutContent) - - self?.openChatForThemeChange() + self?.giftPremium() }))) } - - if strongSelf.peerId.namespace == Namespaces.Peer.CloudUser && user.botInfo == nil && !user.flags.contains(.isSupport) { - items.append(.action(ContextMenuActionItem(text: presentationData.strings.UserInfo_StartSecretChat, icon: { theme in - generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Lock"), color: theme.contextMenu.primaryColor) - }, action: { _, f in - f(.dismissWithoutContent) + + let itemsCount = items.count + + if canSetupAutoremoveTimeout { + let strings = strongSelf.presentationData.strings + items.append(.action(ContextMenuActionItem(text: currentAutoremoveTimeout == nil ? strongSelf.presentationData.strings.PeerInfo_EnableAutoDelete : strongSelf.presentationData.strings.PeerInfo_AdjustAutoDelete, icon: { theme in + if let currentAutoremoveTimeout = currentAutoremoveTimeout { + let text = NSAttributedString(string: shortTimeIntervalString(strings: strings, value: currentAutoremoveTimeout), font: Font.regular(14.0), textColor: theme.contextMenu.primaryColor) + let bounds = text.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil) + return generateImage(bounds.size.integralFloor, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + UIGraphicsPushContext(context) + text.draw(in: bounds) + UIGraphicsPopContext() + }) + } else { + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Timer"), color: theme.contextMenu.primaryColor) + } + }, action: { [weak self] c, _ in + var subItems: [ContextMenuItem] = [] - self?.openStartSecretChat() + subItems.append(.action(ContextMenuActionItem(text: strings.Common_Back, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor) + }, action: { c, _ in + c.popItems() + }))) + subItems.append(.separator) + + let presetValues: [Int32] = [ + 1 * 24 * 60 * 60, + 7 * 24 * 60 * 60, + 31 * 24 * 60 * 60 + ] + + for value in presetValues { + subItems.append(.action(ContextMenuActionItem(text: timeIntervalString(strings: strings, value: value), icon: { _ in + return nil + }, action: { _, f in + f(.default) + + self?.setAutoremove(timeInterval: value) + }))) + } + + subItems.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.PeerInfo_AutoDeleteSettingOther, icon: { _ in + return nil + }, action: { _, f in + f(.default) + + self?.openAutoremove(currentValue: currentAutoremoveTimeout) + }))) + + if let _ = currentAutoremoveTimeout { + subItems.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.PeerInfo_AutoDeleteDisable, textColor: .destructive, icon: { _ in + return nil + }, action: { _, f in + f(.default) + + self?.setAutoremove(timeInterval: nil) + }))) + } + + subItems.append(.separator) + + subItems.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.PeerInfo_AutoDeleteInfo, textLayout: .multiline, textFont: .small, icon: { _ in + return nil + }, action: nil as ((ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void) -> Void)?))) + + c.pushItems(items: .single(ContextController.Items(content: .list(subItems)))) }))) } @@ -4163,6 +4168,12 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate }))) } } + + let finalItemsCount = items.count + + if finalItemsCount > itemsCount { + items.insert(.separator, at: itemsCount) + } } else if let channel = peer as? TelegramChannel { if let cachedData = strongSelf.data?.cachedData as? CachedChannelData, cachedData.flags.contains(.canViewStats) { items.append(.action(ContextMenuActionItem(text: presentationData.strings.ChannelInfo_Stats, icon: { theme in @@ -6408,6 +6419,11 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate } })) } + + private func giftPremium() { + let controller = PremiumGiftScreen(context: self.context, peerId: self.peerId) + self.controller?.push(controller) + } fileprivate func switchToAccount(id: AccountRecordId) { self.accountsAndPeers.set(.never()) diff --git a/submodules/TelegramUI/Sources/ShareExtensionContext.swift b/submodules/TelegramUI/Sources/ShareExtensionContext.swift index bd586a1c7f..308fc0332b 100644 --- a/submodules/TelegramUI/Sources/ShareExtensionContext.swift +++ b/submodules/TelegramUI/Sources/ShareExtensionContext.swift @@ -237,7 +237,7 @@ public class ShareRootControllerImpl { return nil }) - let sharedContext = SharedAccountContextImpl(mainWindow: nil, sharedContainerPath: self.initializationData.appGroupPath, basePath: rootPath, encryptionParameters: ValueBoxEncryptionParameters(forceEncryptionIfNoSet: false, key: ValueBoxEncryptionParameters.Key(data: self.initializationData.encryptionParameters.0)!, salt: ValueBoxEncryptionParameters.Salt(data: self.initializationData.encryptionParameters.1)!), accountManager: accountManager, appLockContext: appLockContext, applicationBindings: applicationBindings, initialPresentationDataAndSettings: initialPresentationDataAndSettings!, networkArguments: NetworkInitializationArguments(apiId: self.initializationData.apiId, apiHash: self.initializationData.apiHash, languagesCategory: self.initializationData.languagesCategory, appVersion: self.initializationData.appVersion, voipMaxLayer: 0, voipVersions: [], appData: .single(self.initializationData.bundleData), autolockDeadine: .single(nil), encryptionProvider: OpenSSLEncryptionProvider(), resolvedDeviceName: nil), premiumProductId: nil, rootPath: rootPath, legacyBasePath: nil, apsNotificationToken: .never(), voipNotificationToken: .never(), setNotificationCall: { _ in }, navigateToChat: { _, _, _ in }) + let sharedContext = SharedAccountContextImpl(mainWindow: nil, sharedContainerPath: self.initializationData.appGroupPath, basePath: rootPath, encryptionParameters: ValueBoxEncryptionParameters(forceEncryptionIfNoSet: false, key: ValueBoxEncryptionParameters.Key(data: self.initializationData.encryptionParameters.0)!, salt: ValueBoxEncryptionParameters.Salt(data: self.initializationData.encryptionParameters.1)!), accountManager: accountManager, appLockContext: appLockContext, applicationBindings: applicationBindings, initialPresentationDataAndSettings: initialPresentationDataAndSettings!, networkArguments: NetworkInitializationArguments(apiId: self.initializationData.apiId, apiHash: self.initializationData.apiHash, languagesCategory: self.initializationData.languagesCategory, appVersion: self.initializationData.appVersion, voipMaxLayer: 0, voipVersions: [], appData: .single(self.initializationData.bundleData), autolockDeadine: .single(nil), encryptionProvider: OpenSSLEncryptionProvider(), resolvedDeviceName: nil), hasInAppPurchases: false, rootPath: rootPath, legacyBasePath: nil, apsNotificationToken: .never(), voipNotificationToken: .never(), setNotificationCall: { _ in }, navigateToChat: { _, _, _ in }) presentationDataPromise.set(sharedContext.presentationData) internalContext = InternalContext(sharedContext: sharedContext) globalInternalContext = internalContext diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index ec89f0bb4b..066c92a5bd 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -90,7 +90,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { public let contactDataManager: DeviceContactDataManager? public let locationManager: DeviceLocationManager? public var callManager: PresentationCallManager? - let premiumProductId: String? + let hasInAppPurchases: Bool private var callDisposable: Disposable? private var callStateDisposable: Disposable? @@ -164,7 +164,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { private var spotlightDataContext: SpotlightDataContext? private var widgetDataContext: WidgetDataContext? - public init(mainWindow: Window1?, sharedContainerPath: String, basePath: String, encryptionParameters: ValueBoxEncryptionParameters, accountManager: AccountManager, appLockContext: AppLockContext, applicationBindings: TelegramApplicationBindings, initialPresentationDataAndSettings: InitialPresentationDataAndSettings, networkArguments: NetworkInitializationArguments, premiumProductId: String?, rootPath: String, legacyBasePath: String?, apsNotificationToken: Signal, voipNotificationToken: Signal, setNotificationCall: @escaping (PresentationCall?) -> Void, navigateToChat: @escaping (AccountRecordId, PeerId, MessageId?) -> Void, displayUpgradeProgress: @escaping (Float?) -> Void = { _ in }) { + public init(mainWindow: Window1?, sharedContainerPath: String, basePath: String, encryptionParameters: ValueBoxEncryptionParameters, accountManager: AccountManager, appLockContext: AppLockContext, applicationBindings: TelegramApplicationBindings, initialPresentationDataAndSettings: InitialPresentationDataAndSettings, networkArguments: NetworkInitializationArguments, hasInAppPurchases: Bool, rootPath: String, legacyBasePath: String?, apsNotificationToken: Signal, voipNotificationToken: Signal, setNotificationCall: @escaping (PresentationCall?) -> Void, navigateToChat: @escaping (AccountRecordId, PeerId, MessageId?) -> Void, displayUpgradeProgress: @escaping (Float?) -> Void = { _ in }) { assert(Queue.mainQueue().isCurrent()) precondition(!testHasInstance) @@ -178,7 +178,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { self.navigateToChatImpl = navigateToChat self.displayUpgradeProgress = displayUpgradeProgress self.appLockContext = appLockContext - self.premiumProductId = premiumProductId + self.hasInAppPurchases = hasInAppPurchases self.accountManager.mediaBox.fetchCachedResourceRepresentation = { (resource, representation) -> Signal in return fetchCachedSharedResourceRepresentation(accountManager: accountManager, resource: resource, representation: representation)