diff --git a/Telegram/Telegram-iOS/Resources/IntroPassword.tgs b/Telegram/Telegram-iOS/Resources/IntroPassword.tgs new file mode 100644 index 0000000000..07cca60ece Binary files /dev/null and b/Telegram/Telegram-iOS/Resources/IntroPassword.tgs differ diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 39753ba6a8..46d06f4555 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -8040,3 +8040,9 @@ Sorry for the inconvenience."; "Login.CancelEmailVerification" = "Do you want to stop the email verification process?"; "Login.CancelEmailVerificationStop" = "Stop"; "Login.CancelEmailVerificationContinue" = "Continue"; + +"Premium.InfiniteReactions" = "Infinite Reactions"; +"Premium.InfiniteReactionsInfo" = "React with thousands of emoji — with multiple reactions per message."; + +"Premium.EmojiStatus" = "Emoji Status"; +"Premium.EmojiStatusInfo" = "Add any of thousands emojis next to your name to display current activity."; diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequencePasswordEntryController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequencePasswordEntryController.swift index 6d339f1532..b7c2bd1f6c 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequencePasswordEntryController.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequencePasswordEntryController.swift @@ -10,6 +10,8 @@ final class AuthorizationSequencePasswordEntryController: ViewController { return self.displayNode as! AuthorizationSequencePasswordEntryControllerNode } + private var validLayout: ContainerViewLayout? + private let presentationData: PresentationData var loginWithPassword: ((String) -> Void)? @@ -33,12 +35,7 @@ final class AuthorizationSequencePasswordEntryController: ViewController { var inProgress: Bool = false { didSet { - if self.inProgress { - let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: self.presentationData.theme.rootController.navigationBar.accentTextColor)) - self.navigationItem.rightBarButtonItem = item - } else { - self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Next, style: .done, target: self, action: #selector(self.nextPressed)) - } + self.updateNavigationItems() self.controllerNode.inProgress = self.inProgress } } @@ -60,8 +57,6 @@ final class AuthorizationSequencePasswordEntryController: ViewController { self.navigationBar?.backPressed = { back() } - - self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Next, style: .done, target: self, action: #selector(self.nextPressed)) } required init(coder aDecoder: NSCoder) { @@ -97,6 +92,19 @@ final class AuthorizationSequencePasswordEntryController: ViewController { self.controllerNode.activateInput() } + func updateNavigationItems() { + guard let layout = self.validLayout, layout.size.width < 360.0 else { + return + } + + if self.inProgress { + let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: self.presentationData.theme.rootController.navigationBar.accentTextColor)) + self.navigationItem.rightBarButtonItem = item + } else { + self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Next, style: .done, target: self, action: #selector(self.nextPressed)) + } + } + func updateData(hint: String, suggestReset: Bool) { if self.hint != hint || self.suggestReset != suggestReset { self.hint = hint @@ -109,6 +117,7 @@ final class AuthorizationSequencePasswordEntryController: ViewController { func passwordIsInvalid() { if self.isNodeLoaded { + self.hapticFeedback.error() self.controllerNode.passwordIsInvalid() } } @@ -116,6 +125,13 @@ final class AuthorizationSequencePasswordEntryController: ViewController { override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) + let hadLayout = self.validLayout != nil + self.validLayout = layout + + if !hadLayout { + self.updateNavigationItems() + } + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequencePasswordEntryControllerNode.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequencePasswordEntryControllerNode.swift index f955e75a11..968adc3235 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequencePasswordEntryControllerNode.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequencePasswordEntryControllerNode.swift @@ -2,17 +2,23 @@ import Foundation import UIKit import AsyncDisplayKit import Display +import SwiftSignalKit import TelegramPresentationData import AuthorizationUtils +import AnimatedStickerNode +import TelegramAnimatedStickerNode +import SolidRoundedButtonNode final class AuthorizationSequencePasswordEntryControllerNode: ASDisplayNode, UITextFieldDelegate { private let strings: PresentationStrings private let theme: PresentationTheme + private let animationNode: AnimatedStickerNode private let titleNode: ASTextNode private let noticeNode: ASTextNode private let forgotNode: HighlightableButtonNode private let resetNode: HighlightableButtonNode + private let proceedNode: SolidRoundedButtonNode private let codeField: TextFieldNode private let codeSeparatorNode: ASDisplayNode @@ -35,13 +41,26 @@ final class AuthorizationSequencePasswordEntryControllerNode: ASDisplayNode, UIT var inProgress: Bool = false { didSet { self.codeField.alpha = self.inProgress ? 0.6 : 1.0 + + if self.inProgress != oldValue { + if self.inProgress { + self.proceedNode.transitionToProgress() + } else { + self.proceedNode.transitionFromProgress() + } + } } } + private var timer: SwiftSignalKit.Timer? + init(strings: PresentationStrings, theme: PresentationTheme) { self.strings = strings self.theme = theme + self.animationNode = DefaultAnimatedStickerNodeImpl() + self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "IntroPassword"), width: 256, height: 256, playbackMode: .still(.start), mode: .direct(cachePathPrefix: nil)) + self.titleNode = ASTextNode() self.titleNode.isUserInteractionEnabled = false self.titleNode.displaysAsynchronously = false @@ -75,6 +94,10 @@ final class AuthorizationSequencePasswordEntryControllerNode: ASDisplayNode, UIT self.codeField.textField.disableAutomaticKeyboardHandling = [.forward, .backward] self.codeField.textField.tintColor = self.theme.list.itemAccentColor + self.proceedNode = SolidRoundedButtonNode(title: self.strings.Login_Continue, theme: SolidRoundedButtonTheme(theme: self.theme), height: 50.0, cornerRadius: 11.0, gloss: false) + self.proceedNode.progressType = .embedded + self.proceedNode.isEnabled = false + super.init() self.setViewBlock({ @@ -84,6 +107,7 @@ final class AuthorizationSequencePasswordEntryControllerNode: ASDisplayNode, UIT self.backgroundColor = self.theme.list.plainBackgroundColor self.codeField.textField.delegate = self + self.codeField.textField.addTarget(self, action: #selector(self.textDidChange), for: .editingChanged) self.addSubnode(self.codeSeparatorNode) self.addSubnode(self.codeField) @@ -91,9 +115,26 @@ final class AuthorizationSequencePasswordEntryControllerNode: ASDisplayNode, UIT self.addSubnode(self.forgotNode) self.addSubnode(self.resetNode) self.addSubnode(self.noticeNode) + self.addSubnode(self.animationNode) + self.addSubnode(self.proceedNode) self.forgotNode.addTarget(self, action: #selector(self.forgotPressed), forControlEvents: .touchUpInside) self.resetNode.addTarget(self, action: #selector(self.resetPressed), forControlEvents: .touchUpInside) + + self.proceedNode.pressed = { [weak self] in + if let strongSelf = self { + strongSelf.loginWithCode?(strongSelf.currentPassword) + } + } + + self.timer = SwiftSignalKit.Timer(timeout: 7.5, repeat: true, completion: { [weak self] in + self?.animationNode.playOnce() + }, queue: Queue.mainQueue()) + self.timer?.start() + } + + deinit { + self.timer?.invalidate() } func updateData(hint: String, didForgotWithNoRecovery: Bool, suggestReset: Bool) { @@ -109,26 +150,33 @@ final class AuthorizationSequencePasswordEntryControllerNode: ASDisplayNode, UIT self.layoutArguments = (layout, navigationBarHeight) var insets = layout.insets(options: []) - insets.top = navigationBarHeight - - if let inputHeight = layout.inputHeight { - insets.bottom += max(inputHeight, layout.standardInputHeight) + insets.top = layout.statusBarHeight ?? 20.0 + if let inputHeight = layout.inputHeight, !inputHeight.isZero { + insets.bottom = max(inputHeight, insets.bottom) } + let titleInset: CGFloat = layout.size.width > 320.0 ? 18.0 : 0.0 + let additionalBottomInset: CGFloat = layout.size.width > 320.0 ? 110.0 : 20.0 + self.titleNode.attributedText = NSAttributedString(string: self.strings.LoginPassword_Title, font: Font.semibold(28.0), textColor: self.theme.list.itemPrimaryTextColor) + let inset: CGFloat = 24.0 + + let animationSize = CGSize(width: 100.0, height: 100.0) let titleSize = self.titleNode.measure(CGSize(width: layout.size.width, height: CGFloat.greatestFiniteMagnitude)) let noticeSize = self.noticeNode.measure(CGSize(width: layout.size.width - 28.0, height: CGFloat.greatestFiniteMagnitude)) let forgotSize = self.forgotNode.measure(CGSize(width: layout.size.width, height: CGFloat.greatestFiniteMagnitude)) let resetSize = self.resetNode.measure(CGSize(width: layout.size.width, height: CGFloat.greatestFiniteMagnitude)) + let proceedHeight = self.proceedNode.updateLayout(width: layout.size.width - inset * 2.0, transition: transition) + let proceedSize = CGSize(width: layout.size.width - inset * 2.0, height: proceedHeight) var items: [AuthorizationLayoutItem] = [] - items.append(AuthorizationLayoutItem(node: self.titleNode, size: titleSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0))) + items.append(AuthorizationLayoutItem(node: self.titleNode, size: titleSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: titleInset, maxValue: titleInset), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0))) items.append(AuthorizationLayoutItem(node: self.noticeNode, size: noticeSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 18.0, maxValue: 18.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0))) - items.append(AuthorizationLayoutItem(node: self.codeField, size: CGSize(width: layout.size.width - 88.0, height: 44.0), spacingBefore: AuthorizationLayoutItemSpacing(weight: 32.0, maxValue: 60.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0))) - items.append(AuthorizationLayoutItem(node: self.codeSeparatorNode, size: CGSize(width: layout.size.width - 88.0, height: UIScreenPixel), spacingBefore: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0))) + items.append(AuthorizationLayoutItem(node: self.codeField, size: CGSize(width: layout.size.width - 80.0, height: 44.0), spacingBefore: AuthorizationLayoutItemSpacing(weight: 32.0, maxValue: 60.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0))) + items.append(AuthorizationLayoutItem(node: self.codeSeparatorNode, size: CGSize(width: layout.size.width - 48.0, height: UIScreenPixel), spacingBefore: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0))) items.append(AuthorizationLayoutItem(node: self.forgotNode, size: forgotSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 48.0, maxValue: 100.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0))) @@ -139,7 +187,22 @@ final class AuthorizationSequencePasswordEntryControllerNode: ASDisplayNode, UIT self.resetNode.isHidden = true } - let _ = layoutAuthorizationItems(bounds: CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: layout.size.height - insets.top - insets.bottom - 20.0)), items: items, transition: transition, failIfDoesNotFit: false) + if layout.size.width > 320.0 { + items.insert(AuthorizationLayoutItem(node: self.animationNode, size: animationSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 10.0, maxValue: 10.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)), at: 0) + self.proceedNode.isHidden = false + self.animationNode.isHidden = false + self.animationNode.visibility = true + } else { + insets.top = navigationBarHeight + self.proceedNode.isHidden = true + self.animationNode.isHidden = true + } + + transition.updateFrame(node: self.proceedNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - proceedSize.width) / 2.0), y: layout.size.height - insets.bottom - proceedSize.height - inset), size: proceedSize)) + + self.animationNode.updateLayout(size: animationSize) + + let _ = layoutAuthorizationItems(bounds: CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: layout.size.height - insets.top - insets.bottom - additionalBottomInset)), items: items, transition: transition, failIfDoesNotFit: false) } func activateInput() { @@ -154,6 +217,10 @@ final class AuthorizationSequencePasswordEntryControllerNode: ASDisplayNode, UIT self.clearOnce = true } + @objc func textDidChange() { + self.proceedNode.isEnabled = !(self.codeField.textField.text ?? "").isEmpty + } + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { if self.clearOnce { self.clearOnce = false diff --git a/submodules/PremiumUI/Resources/swirl.scn b/submodules/PremiumUI/Resources/swirl.scn index 8811158627..914bc311d4 100644 Binary files a/submodules/PremiumUI/Resources/swirl.scn and b/submodules/PremiumUI/Resources/swirl.scn differ diff --git a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift index 3725b2ad2c..ce6aba7c75 100644 --- a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift @@ -712,7 +712,7 @@ private final class DemoSheetContent: CombinedComponent { isStandalone = true } - if let reactions = state.reactions, let stickers = state.stickers, let appIcons = state.appIcons, let configuration = state.promoConfiguration { + if let stickers = state.stickers, let appIcons = state.appIcons, let configuration = state.promoConfiguration { let textColor = theme.actionSheet.primaryTextColor var availableItems: [PremiumPerk: DemoPagerComponent.Item] = [:] @@ -794,15 +794,14 @@ private final class DemoSheetContent: CombinedComponent { id: PremiumDemoScreen.Subject.uniqueReactions, component: AnyComponent( PageComponent( - content: AnyComponent( - ReactionsCarouselComponent( - context: component.context, - theme: environment.theme, - reactions: reactions - ) - ), - title: isStandalone ? strings.Premium_ReactionsStandalone : strings.Premium_Reactions, - text: isStandalone ? strings.Premium_ReactionsStandaloneInfo : strings.Premium_ReactionsInfo, + content: AnyComponent(PhoneDemoComponent( + context: component.context, + position: .top, + videoFile: configuration.videos["infinite_reactions"], + decoration: .badgeStars + )), + title: strings.Premium_InfiniteReactions, + text: strings.Premium_InfiniteReactionsInfo, textColor: textColor ) ) @@ -826,6 +825,24 @@ private final class DemoSheetContent: CombinedComponent { ) ) ) + availableItems[.emojiStatus] = DemoPagerComponent.Item( + AnyComponentWithIdentity( + id: PremiumDemoScreen.Subject.emojiStatus, + component: AnyComponent( + PageComponent( + content: AnyComponent(PhoneDemoComponent( + context: component.context, + position: .top, + videoFile: configuration.videos["emoji_status"], + decoration: .swirlStars + )), + title: strings.Premium_EmojiStatus, + text: strings.Premium_EmojiStatusInfo, + textColor: textColor + ) + ) + ) + ) availableItems[.advancedChatManagement] = DemoPagerComponent.Item( AnyComponentWithIdentity( id: PremiumDemoScreen.Subject.advancedChatManagement, @@ -1171,6 +1188,7 @@ public class PremiumDemoScreen: ViewControllerComponentContainer { case animatedUserpics case appIcons case animatedEmoji + case emojiStatus } public enum Source: Equatable { diff --git a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift index c5e48d9060..d408f70431 100644 --- a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift @@ -397,6 +397,8 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent { demoSubject = .appIcons case .animatedEmoji: demoSubject = .animatedEmoji + case .emojiStatus: + demoSubject = .emojiStatus } let controller = PremiumDemoScreen( diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index e367595fd5..39a89efc44 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -54,7 +54,7 @@ public enum PremiumSource: Equatable { case .stickers: return "premium_stickers" case .reactions: - return "unique_reactions" + return "infinite_reactions" case .ads: return "no_ads" case .upload: @@ -110,6 +110,7 @@ enum PremiumPerk: CaseIterable { case animatedUserpics case appIcons case animatedEmoji + case emojiStatus static var allCases: [PremiumPerk] { return [ @@ -124,7 +125,8 @@ enum PremiumPerk: CaseIterable { .profileBadge, .animatedUserpics, .appIcons, - .animatedEmoji + .animatedEmoji, + .emojiStatus ] } @@ -151,7 +153,7 @@ enum PremiumPerk: CaseIterable { case .noAds: return "no_ads" case .uniqueReactions: - return "unique_reactions" + return "infinite_reactions" case .premiumStickers: return "premium_stickers" case .advancedChatManagement: @@ -164,6 +166,8 @@ enum PremiumPerk: CaseIterable { return "app_icons" case .animatedEmoji: return "animated_emoji" + case .emojiStatus: + return "emoji_status" } } @@ -180,7 +184,7 @@ enum PremiumPerk: CaseIterable { case .noAds: return strings.Premium_NoAds case .uniqueReactions: - return strings.Premium_Reactions + return strings.Premium_InfiniteReactions case .premiumStickers: return strings.Premium_Stickers case .advancedChatManagement: @@ -193,6 +197,8 @@ enum PremiumPerk: CaseIterable { return strings.Premium_AppIcon case .animatedEmoji: return strings.Premium_AnimatedEmoji + case .emojiStatus: + return strings.Premium_EmojiStatus } } @@ -209,7 +215,7 @@ enum PremiumPerk: CaseIterable { case .noAds: return strings.Premium_NoAdsInfo case .uniqueReactions: - return strings.Premium_ReactionsInfo + return strings.Premium_InfiniteReactionsInfo case .premiumStickers: return strings.Premium_StickersInfo case .advancedChatManagement: @@ -222,6 +228,8 @@ enum PremiumPerk: CaseIterable { return strings.Premium_AppIconInfo case .animatedEmoji: return strings.Premium_AnimatedEmojiInfo + case .emojiStatus: + return strings.Premium_EmojiStatusInfo } } @@ -251,6 +259,8 @@ enum PremiumPerk: CaseIterable { return "Premium/Perk/AppIcon" case .animatedEmoji: return "Premium/Perk/Emoji" + case .emojiStatus: + return "Premium/Perk/Emoji" } } } @@ -263,6 +273,7 @@ struct PremiumIntroConfiguration { .fasterDownload, .voiceToText, .noAds, + .emojiStatus, .uniqueReactions, .premiumStickers, .animatedEmoji, @@ -1445,6 +1456,8 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { demoSubject = .appIcons case .animatedEmoji: demoSubject = .animatedEmoji + case .emojiStatus: + demoSubject = .emojiStatus } let controller = PremiumDemoScreen( diff --git a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift index e1598799b9..627674889f 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift @@ -1107,7 +1107,7 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting } navigationController.setViewControllers(controllers, animated: true) - Queue.mainQueue().after(0.1, { + Queue.mainQueue().after(0.5, { navigationController.presentOverlay(controller: UndoOverlayController(presentationData: presentationData, content: .emoji(name: "IntroLetter", text: presentationData.strings.Login_EmailChanged), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false })) diff --git a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift index 35884fb9d8..2f6cf77318 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -555,7 +555,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { emojiFile = item.associatedData.animatedEmojiStickers[emoji.strippedEmoji]?.first?.file } - if item.message.text.count == 1, item.message.associatedMedia.isEmpty && emojiFile != nil { + if item.message.text.count == 1, (item.message.textEntitiesAttribute?.entities ?? []).isEmpty && emojiFile != nil { emojiString = nil } else if emojiString != nil { emojiFile = nil