Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
+50
View File
@@ -0,0 +1,50 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
filegroup(
name = "PasswordSetupUIResources",
srcs = glob([
"Resources/**/*",
], exclude = ["Resources/**/.*"]),
visibility = ["//visibility:public"],
)
filegroup(
name = "PasswordSetupUIAssets",
srcs = glob(["PasswordSetupUIImages.xcassets/**"]),
visibility = ["//visibility:public"],
)
swift_library(
name = "PasswordSetupUI",
module_name = "PasswordSetupUI",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/Postbox:Postbox",
"//submodules/TelegramCore:TelegramCore",
"//submodules/AccountContext:AccountContext",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/ProgressNavigationButtonNode:ProgressNavigationButtonNode",
"//submodules/ActivityIndicator:ActivityIndicator",
"//submodules/AlertUI:AlertUI",
"//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/ItemListUI:ItemListUI",
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
"//submodules/AppBundle:AppBundle",
"//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode",
"//submodules/OverlayStatusController:OverlayStatusController",
"//submodules/rlottie:RLottieBinding",
"//submodules/ManagedAnimationNode:ManagedAnimationNode",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}
@@ -0,0 +1,9 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
},
"properties" : {
"provides-namespace" : true
}
}
@@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "User.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
@@ -0,0 +1,170 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import RLottieBinding
import AppBundle
import GZip
import SwiftSignalKit
import ManagedAnimationNode
enum ManagedMonkeyAnimationIdle: CaseIterable {
case blink
case ear
case still
}
enum ManagedMonkeyAnimationState: Equatable {
case idle(ManagedMonkeyAnimationIdle)
case eyesClosed
case peeking
case tracking(CGFloat)
}
final class ManagedMonkeyAnimationNode: ManagedAnimationNode {
private var monkeyState: ManagedMonkeyAnimationState = .idle(.blink)
private var timer: SwiftSignalKit.Timer?
init() {
super.init(size: CGSize(width: 136.0, height: 136.0))
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyIdle"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.3))
}
deinit {
self.timer?.invalidate()
}
private func startIdleTimer() {
self.timer?.invalidate()
let timer = SwiftSignalKit.Timer(timeout: Double.random(in: 1.0 ..< 1.5), repeat: false, completion: { [weak self] in
guard let strongSelf = self else {
return
}
switch strongSelf.monkeyState {
case .idle:
if let idle = ManagedMonkeyAnimationIdle.allCases.randomElement() {
strongSelf.setState(.idle(idle))
}
default:
break
}
}, queue: .mainQueue())
self.timer = timer
timer.start()
}
override func advanceState() {
super.advanceState()
self.timer?.invalidate()
self.timer = nil
if self.trackStack.isEmpty, case .idle = self.monkeyState {
self.startIdleTimer()
}
}
private func enqueueIdle(_ idle: ManagedMonkeyAnimationIdle) {
switch idle {
case .still:
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyIdle"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.3))
case .blink:
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyIdle1"), frames: .range(startFrame: 0, endFrame: 30), duration: 0.3))
case .ear:
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyIdle2"), frames: .range(startFrame: 0, endFrame: 30), duration: 0.3))
//self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyIdle"), frames: .range(startFrame: 0, endFrame: 179), duration: 3.0))
}
}
func setState(_ monkeyState: ManagedMonkeyAnimationState) {
let previousState = self.monkeyState
self.monkeyState = monkeyState
self.timer?.invalidate()
self.timer = nil
func enqueueTracking(_ value: CGFloat) {
let lowerBound = 18
let upperBound = 160
let frameIndex = lowerBound + Int(value * CGFloat(upperBound - lowerBound))
if let state = self.state, state.item.source == .local("TwoFactorSetupMonkeyTracking") {
let item = ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyTracking"), frames: .range(startFrame: state.frameIndex ?? 0, endFrame: frameIndex), duration: 0.3)
self.state = ManagedAnimationState(displaySize: self.intrinsicSize, item: item, current: state)
self.didTryAdvancingState = false
self.updateAnimation()
} else {
self.trackStack = self.trackStack.filter {
$0.source != .local("TwoFactorSetupMonkeyTracking")
}
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyTracking"), frames: .range(startFrame: 0, endFrame: frameIndex), duration: 0.3))
}
}
func enqueueClearTracking() {
if let state = self.state, state.item.source == .local("TwoFactorSetupMonkeyTracking") {
let item = ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyTracking"), frames: .range(startFrame: state.frameIndex ?? 0, endFrame: 0), duration: 0.3)
self.state = ManagedAnimationState(displaySize: self.intrinsicSize, item: item, current: state)
self.didTryAdvancingState = false
self.updateAnimation()
}
}
switch previousState {
case .idle:
switch monkeyState {
case let .idle(idle):
self.enqueueIdle(idle)
case .eyesClosed:
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyClose"), frames: .range(startFrame: 0, endFrame: 41), duration: 0.3))
case .peeking:
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyCloseAndPeek"), frames: .range(startFrame: 0, endFrame: 41), duration: 0.3))
case let .tracking(value):
enqueueTracking(value)
}
case .eyesClosed:
switch monkeyState {
case let .idle(idle):
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyClose"), frames: .range(startFrame: 41, endFrame: 0), duration: 0.3))
self.enqueueIdle(idle)
case .eyesClosed:
break
case .peeking:
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyPeek"), frames: .range(startFrame: 0, endFrame: 14), duration: 0.3))
case let .tracking(value):
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyClose"), frames: .range(startFrame: 41, endFrame: 0), duration: 0.3))
enqueueTracking(value)
}
case .peeking:
switch monkeyState {
case let .idle(idle):
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyCloseAndPeek"), frames: .range(startFrame: 41, endFrame: 0), duration: 0.3))
self.enqueueIdle(idle)
case .eyesClosed:
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyPeek"), frames: .range(startFrame: 14, endFrame: 0), duration: 0.3))
case .peeking:
break
case let .tracking(value):
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyCloseAndPeek"), frames: .range(startFrame: 41, endFrame: 0), duration: 0.3))
enqueueTracking(value)
}
case let .tracking(currentValue):
switch monkeyState {
case let .idle(idle):
enqueueClearTracking()
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyIdle"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.3))
self.enqueueIdle(idle)
case .eyesClosed:
enqueueClearTracking()
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyClose"), frames: .range(startFrame: 0, endFrame: 41), duration: 0.3))
case .peeking:
enqueueClearTracking()
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyCloseAndPeek"), frames: .range(startFrame: 0, endFrame: 41), duration: 0.3))
case let .tracking(value):
if abs(currentValue - value) > CGFloat.ulpOfOne {
enqueueTracking(value)
}
}
}
}
}
@@ -0,0 +1,252 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import AccountContext
import AlertUI
import PresentationDataUtils
private final class ResetPasswordControllerArguments {
let context: AccountContext
let updateCodeText: (String) -> Void
let openHelp: () -> Void
init(context: AccountContext, updateCodeText: @escaping (String) -> Void, openHelp: @escaping () -> Void) {
self.context = context
self.updateCodeText = updateCodeText
self.openHelp = openHelp
}
}
private enum ResetPasswordSection: Int32 {
case code
case help
}
private enum ResetPasswordEntryTag: ItemListItemTag {
case code
func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? ResetPasswordEntryTag {
return self == other
} else {
return false
}
}
}
private enum ResetPasswordEntry: ItemListNodeEntry, Equatable {
case code(PresentationTheme, PresentationStrings, String, String)
case codeInfo(PresentationTheme, String)
case helpInfo(PresentationTheme, String)
var section: ItemListSectionId {
switch self {
case .code, .codeInfo:
return ResetPasswordSection.code.rawValue
case .helpInfo:
return ResetPasswordSection.help.rawValue
}
}
var stableId: Int32 {
switch self {
case .code:
return 0
case .codeInfo:
return 1
case .helpInfo:
return 2
}
}
static func <(lhs: ResetPasswordEntry, rhs: ResetPasswordEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! ResetPasswordControllerArguments
switch self {
case let .code(theme, _, text, value):
return ItemListSingleLineInputItem(presentationData: presentationData, systemStyle: .glass, title: NSAttributedString(string: text, textColor: theme.list.itemPrimaryTextColor), text: value, placeholder: "", type: .number, spacing: 10.0, tag: ResetPasswordEntryTag.code, sectionId: self.section, textUpdated: { updatedText in
arguments.updateCodeText(updatedText)
}, action: {
})
case let .codeInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .helpInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section, linkAction: { action in
if case .tap = action {
arguments.openHelp()
}
})
}
}
}
private struct ResetPasswordControllerState: Equatable {
var code: String = ""
var checking: Bool = false
}
private func resetPasswordControllerEntries(presentationData: PresentationData, state: ResetPasswordControllerState, pattern: String) -> [ResetPasswordEntry] {
var entries: [ResetPasswordEntry] = []
entries.append(.code(presentationData.theme, presentationData.strings, presentationData.strings.TwoStepAuth_RecoveryCode, state.code))
entries.append(.codeInfo(presentationData.theme, presentationData.strings.TwoStepAuth_RecoveryCodeHelp))
let stringData = presentationData.strings.TwoStepAuth_RecoveryEmailUnavailable(pattern)
var string = stringData.string
if let range = stringData.ranges.first {
string.insert(contentsOf: "]()", at: string.index(string.startIndex, offsetBy: range.range.upperBound))
string.insert(contentsOf: "[", at: string.index(string.startIndex, offsetBy: range.range.lowerBound))
}
entries.append(.helpInfo(presentationData.theme, string))
return entries
}
public enum ResetPasswordState: Equatable {
case setup(currentPassword: String?)
case pendingVerification(emailPattern: String)
}
public func resetPasswordController(context: AccountContext, emailPattern: String, completion: @escaping (Bool) -> Void) -> ViewController {
let statePromise = ValuePromise(ResetPasswordControllerState(), ignoreRepeated: true)
let stateValue = Atomic(value: ResetPasswordControllerState())
let updateState: ((ResetPasswordControllerState) -> ResetPasswordControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
var dismissImpl: (() -> Void)?
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)?
let actionsDisposable = DisposableSet()
let saveDisposable = MetaDisposable()
actionsDisposable.add(saveDisposable)
let arguments = ResetPasswordControllerArguments(context: context, updateCodeText: { updatedText in
updateState { state in
var state = state
state.code = updatedText
return state
}
}, openHelp: {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(textAlertController(context: context, title: presentationData.strings.TwoStepAuth_RecoveryUnavailableResetTitle, text: presentationData.strings.TwoStepAuth_RecoveryEmailResetText, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.TwoStepAuth_RecoveryUnavailableResetAction, action: {
let _ = (context.engine.auth.requestTwoStepPasswordReset()
|> deliverOnMainQueue).start(next: { result in
switch result {
case .done, .waitingForReset:
completion(false)
case .declined:
break
case .error:
break
}
})
})]), ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
})
var initialFocusImpl: (() -> Void)?
let signal = combineLatest(context.sharedContext.presentationData, statePromise.get())
|> deliverOnMainQueue
|> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: {
dismissImpl?()
})
var rightNavigationButton: ItemListNavigationButton?
if state.checking {
rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {})
} else {
rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: !state.code.isEmpty, action: {
var state: ResetPasswordControllerState?
updateState { s in
state = s
return s
}
if let state = state, !state.checking, !state.code.isEmpty {
updateState { state in
var state = state
state.checking = true
return state
}
saveDisposable.set((context.engine.auth.performPasswordRecovery(code: state.code, updatedPassword: .none)
|> deliverOnMainQueue).start(error: { error in
updateState { state in
var state = state
state.checking = false
return state
}
let text: String
switch error {
case .invalidCode:
text = presentationData.strings.TwoStepAuth_RecoveryCodeInvalid
case .expired:
text = presentationData.strings.TwoStepAuth_RecoveryCodeExpired
case .limitExceeded:
text = presentationData.strings.TwoStepAuth_FloodError
case .generic:
text = presentationData.strings.Login_UnknownError
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(textAlertController(context: context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
}, completed: {
completion(true)
}))
}
})
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.TwoStepAuth_RecoveryTitle), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: resetPasswordControllerEntries(presentationData: presentationData, state: state, pattern: emailPattern), style: .blocks, focusItemTag: ResetPasswordEntryTag.code, emptyStateItem: nil, animateChanges: false)
return (controllerState, (listState, arguments))
}
|> afterDisposed {
actionsDisposable.dispose()
}
let controller = ItemListController(context: context, state: signal)
dismissImpl = { [weak controller] in
controller?.view.endEditing(true)
controller?.dismiss()
}
presentControllerImpl = { [weak controller] c, p in
if let controller = controller {
controller.present(c, in: .window(.root), with: p)
}
}
initialFocusImpl = { [weak controller] in
guard let controller = controller, controller.didAppearOnce else {
return
}
var resultItemNode: ItemListSingleLineInputItemNode?
let _ = controller.frameForItemNode({ itemNode in
if let itemNode = itemNode as? ItemListSingleLineInputItemNode, let tag = itemNode.tag, tag.isEqual(to: ResetPasswordEntryTag.code) {
resultItemNode = itemNode
return true
}
return false
})
if let resultItemNode = resultItemNode {
resultItemNode.focus()
}
}
controller.didAppear = { firstTime in
if !firstTime {
return
}
initialFocusImpl?()
}
return controller
}
@@ -0,0 +1,210 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import TelegramPresentationData
enum SetupTwoStepVerificationInputType {
case password
case text
case code
case email
}
struct SetupTwoStepVerificationContentAction {
let title: String
let action: () -> Void
}
final class SetupTwoStepVerificationContentNode: ASDisplayNode, UITextFieldDelegate {
private var theme: PresentationTheme
let kind: SetupTwoStepVerificationStateKind
private let leftAction: SetupTwoStepVerificationContentAction?
private let rightAction: SetupTwoStepVerificationContentAction?
private let textUpdated: (String) -> Void
private let returnPressed: () -> Void
private let titleNode: ImmediateTextNode
private let subtitleNode: ImmediateTextNode
private let inputNode: TextFieldNode
private let inputSeparator: ASDisplayNode
private let leftActionButton: HighlightableButtonNode
private let rightActionButton: HighlightableButtonNode
private var isEnabled = true
private var clearOnce: Bool = false
init(theme: PresentationTheme, kind: SetupTwoStepVerificationStateKind, title: String, subtitle: String, inputType: SetupTwoStepVerificationInputType, placeholder: String, text: String, isPassword: Bool, textUpdated: @escaping (String) -> Void, returnPressed: @escaping () -> Void, leftAction: SetupTwoStepVerificationContentAction?, rightAction: SetupTwoStepVerificationContentAction?) {
self.theme = theme
self.kind = kind
self.leftAction = leftAction
self.rightAction = rightAction
self.textUpdated = textUpdated
self.returnPressed = returnPressed
self.titleNode = ImmediateTextNode()
self.titleNode.maximumNumberOfLines = 0
self.titleNode.displaysAsynchronously = false
self.titleNode.textAlignment = .center
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.light(30.0), textColor: theme.list.itemPrimaryTextColor, paragraphAlignment: .center)
self.subtitleNode = ImmediateTextNode()
self.subtitleNode.maximumNumberOfLines = 0
self.subtitleNode.displaysAsynchronously = false
self.subtitleNode.textAlignment = .center
self.subtitleNode.attributedText = NSAttributedString(string: subtitle, font: Font.regular(16.0), textColor: theme.list.itemPrimaryTextColor, paragraphAlignment: .center)
self.inputNode = TextFieldNode()
self.inputNode.textField.textColor = theme.list.itemPrimaryTextColor
self.inputNode.textField.font = Font.regular(22.0)
self.inputNode.textField.attributedPlaceholder = NSAttributedString(string: placeholder, font: Font.regular(22.0), textColor: theme.list.itemPlaceholderTextColor)
self.inputNode.textField.textAlignment = .center
self.inputNode.textField.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance
self.inputNode.textField.tintColor = theme.list.itemAccentColor
switch inputType {
case .password:
self.inputNode.textField.isSecureTextEntry = true
self.inputNode.textField.autocapitalizationType = .none
self.inputNode.textField.autocorrectionType = .no
if #available(iOSApplicationExtension 12.0, iOS 12.0, *) {
#if DEBUG
self.inputNode.textField.textContentType = .newPassword
#endif
}
case .text:
break
case .code:
self.inputNode.textField.autocapitalizationType = .none
self.inputNode.textField.autocorrectionType = .no
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
self.inputNode.textField.keyboardType = .asciiCapableNumberPad
} else {
self.inputNode.textField.keyboardType = .numberPad
}
if #available(iOSApplicationExtension 12.0, iOS 12.0, *) {
self.inputNode.textField.textContentType = .oneTimeCode
}
case .email:
self.inputNode.textField.autocapitalizationType = .none
self.inputNode.textField.autocorrectionType = .no
self.inputNode.textField.keyboardType = .emailAddress
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
self.inputNode.textField.textContentType = .emailAddress
}
}
self.inputSeparator = ASDisplayNode()
self.inputSeparator.isLayerBacked = true
self.inputSeparator.backgroundColor = theme.list.itemPlainSeparatorColor
self.leftActionButton = HighlightableButtonNode()
self.leftActionButton.hitTestSlop = UIEdgeInsets(top: -10.0, left: -16.0, bottom: -10.0, right: -16.0)
self.rightActionButton = HighlightableButtonNode()
self.rightActionButton.hitTestSlop = UIEdgeInsets(top: -10.0, left: -16.0, bottom: -10.0, right: -16.0)
super.init()
self.addSubnode(self.titleNode)
self.addSubnode(self.subtitleNode)
self.addSubnode(self.inputNode)
self.addSubnode(self.inputSeparator)
self.inputNode.textField.addTarget(self, action: #selector(self.inputNodeTextChanged(_:)), for: .editingChanged)
self.inputNode.textField.returnKeyType = .next
self.inputNode.textField.delegate = self
if let leftAction = self.leftAction {
self.leftActionButton.setAttributedTitle(NSAttributedString(string: leftAction.title, font: Font.regular(16.0), textColor: theme.list.itemAccentColor), for: [])
self.leftActionButton.setAttributedTitle(NSAttributedString(string: leftAction.title, font: Font.regular(16.0), textColor: theme.list.itemDisabledTextColor), for: [.disabled])
self.addSubnode(self.leftActionButton)
self.leftActionButton.addTarget(self, action: #selector(self.actionButtonPressed(_:)), forControlEvents: .touchUpInside)
}
if let rightAction = self.rightAction {
self.rightActionButton.setAttributedTitle(NSAttributedString(string: rightAction.title, font: Font.regular(16.0), textColor: theme.list.itemAccentColor), for: [])
self.rightActionButton.setAttributedTitle(NSAttributedString(string: rightAction.title, font: Font.regular(16.0), textColor: theme.list.itemDisabledTextColor), for: [.disabled])
self.addSubnode(self.rightActionButton)
self.rightActionButton.addTarget(self, action: #selector(self.actionButtonPressed(_:)), forControlEvents: .touchUpInside)
}
}
func updatePresentationData(_ presentationData: PresentationData) {
self.theme = presentationData.theme
self.inputNode.textField.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance
self.inputSeparator.backgroundColor = self.theme.list.itemPlainSeparatorColor
self.inputNode.textField.tintColor = self.theme.list.itemAccentColor
}
func updateIsEnabled(_ isEnabled: Bool) {
self.isEnabled = isEnabled
self.leftActionButton.isEnabled = isEnabled
self.rightActionButton.isEnabled = isEnabled
}
func updateLayout(size: CGSize, insets: UIEdgeInsets, visibleInsets: UIEdgeInsets, transition: ContainedViewLayoutTransition) {
let sidePadding: CGFloat = 20.0
let sideButtonInset: CGFloat = 16.0
let titleSize = self.titleNode.updateLayout(CGSize(width: size.width - sidePadding * 2.0, height: .greatestFiniteMagnitude))
let subtitleSize = self.subtitleNode.updateLayout(CGSize(width: size.width - sidePadding * 2.0, height: .greatestFiniteMagnitude))
let leftButtonSize = self.leftActionButton.measure(CGSize(width: size.width - sidePadding * 2.0, height: .greatestFiniteMagnitude))
let rightButtonSize = self.rightActionButton.measure(CGSize(width: size.width - sidePadding * 2.0, height: .greatestFiniteMagnitude))
let buttonsHeight: CGFloat
if self.leftActionButton.supernode != nil || self.rightActionButton.supernode != nil {
buttonsHeight = 56.0
} else {
buttonsHeight = 0.0
}
let titleSubtitleSpacing: CGFloat = 12.0
let textHeight = titleSize.height + titleSubtitleSpacing + subtitleSize.height
let inputHeight: CGFloat = 44.0
let inputWidth: CGFloat = min(300.0, size.width - 37.0 * 2.0)
let minContentHeight = textHeight + inputHeight
let contentHeight = min(215.0, max(size.height - insets.top - insets.bottom - 40.0, minContentHeight))
let contentOrigin = max(56.0, insets.top + floor((size.height - insets.top - insets.bottom - contentHeight) / 2.0))
let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: contentOrigin), size: titleSize)
transition.updateFrame(node: self.titleNode, frame: titleFrame)
transition.updateFrame(node: self.subtitleNode, frame: CGRect(origin: CGPoint(x: floor((size.width - subtitleSize.width) / 2.0), y: titleFrame.maxY + titleSubtitleSpacing), size: subtitleSize))
transition.updateFrame(node: self.inputSeparator, frame: CGRect(origin: CGPoint(x: floor((size.width - inputWidth) / 2.0), y: contentOrigin + contentHeight - UIScreenPixel), size: CGSize(width: inputWidth, height: UIScreenPixel)))
transition.updateFrame(node: self.inputNode, frame: CGRect(origin: CGPoint(x: floor((size.width - inputWidth) / 2.0), y: contentOrigin + contentHeight - inputHeight), size: CGSize(width: inputWidth, height: inputHeight)))
transition.updateFrame(node: self.leftActionButton, frame: CGRect(origin: CGPoint(x: sideButtonInset, y: size.height - visibleInsets.bottom - buttonsHeight + floor((buttonsHeight - leftButtonSize.height) / 2.0)), size: leftButtonSize))
transition.updateFrame(node: self.rightActionButton, frame: CGRect(origin: CGPoint(x: size.width - sideButtonInset - rightButtonSize.width, y: size.height - visibleInsets.bottom - buttonsHeight + floor((buttonsHeight - rightButtonSize.height) / 2.0)), size: rightButtonSize))
}
func activate() {
self.inputNode.textField.becomeFirstResponder()
}
func dataEntryError() {
self.clearOnce = true
}
@objc private func inputNodeTextChanged(_ textField: UITextField) {
self.textUpdated(textField.text ?? "")
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if self.clearOnce {
self.clearOnce = false
if range.length > string.count {
textField.text = ""
return false
}
}
return true
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
self.returnPressed()
return false
}
@objc private func actionButtonPressed(_ node: ASDisplayNode) {
if node === self.leftActionButton {
self.leftAction?.action()
} else if node === self.rightActionButton {
self.rightAction?.action()
}
}
}
@@ -0,0 +1,160 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import ProgressNavigationButtonNode
import AccountContext
public class SetupTwoStepVerificationController: ViewController {
private let context: AccountContext
private let initialState: SetupTwoStepVerificationInitialState
private let stateUpdated: (SetupTwoStepVerificationStateUpdate, Bool, SetupTwoStepVerificationController) -> Void
private var controllerNode: SetupTwoStepVerificationControllerNode {
return self.displayNode as! SetupTwoStepVerificationControllerNode
}
private var _ready = Promise<Bool>()
override public var ready: Promise<Bool> {
return self._ready
}
private var didPlayPresentationAnimation = false
private var currentBackAction = false
private var currentNextAction: SetupTwoStepVerificationNextAction?
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
public init(context: AccountContext, initialState: SetupTwoStepVerificationInitialState, stateUpdated: @escaping (SetupTwoStepVerificationStateUpdate, Bool, SetupTwoStepVerificationController) -> Void) {
self.context = context
self.initialState = initialState
self.stateUpdated = stateUpdated
self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: NavigationBarTheme(buttonColor: self.presentationData.theme.rootController.navigationBar.accentTextColor, disabledButtonColor: self.presentationData.theme.rootController.navigationBar.disabledButtonColor, primaryTextColor: self.presentationData.theme.rootController.navigationBar.primaryTextColor, backgroundColor: .clear, enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear), strings: NavigationBarStrings(presentationStrings: self.presentationData.strings)))
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
self.navigationItem.setLeftBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)), animated: false)
self.presentationDataDisposable = (self.context.sharedContext.presentationData
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
if let strongSelf = self {
let previousTheme = strongSelf.presentationData.theme
let previousStrings = strongSelf.presentationData.strings
strongSelf.presentationData = presentationData
if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings {
strongSelf.updateThemeAndStrings()
}
}
})
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.presentationDataDisposable?.dispose()
}
@objc private func backPressed() {
self.controllerNode.activateBackAction()
}
@objc private func cancelPressed() {
self.dismiss()
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if let presentationArguments = self.presentationArguments as? ViewControllerPresentationArguments, !self.didPlayPresentationAnimation {
self.didPlayPresentationAnimation = true
if case .modalSheet = presentationArguments.presentationAnimation {
self.controllerNode.animateIn(completion: presentationArguments.completion)
}
}
}
private func updateThemeAndStrings() {
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData))
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
self.controllerNode.updatePresentationData(self.presentationData)
}
override public func loadDisplayNode() {
self.displayNode = SetupTwoStepVerificationControllerNode(context: self.context, updateBackAction: { [weak self] action in
guard let strongSelf = self else {
return
}
if strongSelf.currentBackAction == action {
return
}
strongSelf.currentBackAction = action
let item: UIBarButtonItem?
if action {
item = UIBarButtonItem(backButtonAppearanceWithTitle: strongSelf.presentationData.strings.Common_Back, target: strongSelf, action: #selector(strongSelf.backPressed))
} else {
item = UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, style: .plain, target: strongSelf, action: #selector(strongSelf.cancelPressed))
}
strongSelf.navigationItem.setLeftBarButton(item, animated: false)
}, updateNextAction: { [weak self] action in
guard let strongSelf = self else {
return
}
if strongSelf.currentNextAction == action {
return
}
strongSelf.currentNextAction = action
let item: UIBarButtonItem?
switch action {
case .none:
item = nil
case .activity:
item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: strongSelf.presentationData.theme.rootController.navigationBar.controlColor))
case let .button(title, _):
item = UIBarButtonItem(title: title, style: .done, target: strongSelf, action: #selector(strongSelf.nextPressed))
}
if let title = item?.title, !title.isEmpty, strongSelf.navigationItem.rightBarButtonItem?.title == title {
} else {
strongSelf.navigationItem.setRightBarButton(item, animated: false)
}
if case let .button(_, isEnabled) = action {
strongSelf.navigationItem.rightBarButtonItem?.isEnabled = isEnabled
}
}, stateUpdated: { [weak self] state, shouldDismiss in
if let strongSelf = self {
strongSelf.stateUpdated(state, shouldDismiss, strongSelf)
}
}, present: { [weak self] c, a in
self?.present(c, in: .window(.root), with: a)
}, dismiss: { [weak self] in
self?.dismiss()
}, initialState: self.initialState)
self._ready.set(.single(true))
self.displayNodeDidLoad()
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
}
@objc private func nextPressed() {
self.controllerNode.activateNextAction()
}
}
@@ -0,0 +1,707 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import TelegramCore
import SwiftSignalKit
import TelegramPresentationData
import ActivityIndicator
import AccountContext
import AlertUI
import PresentationDataUtils
public enum SetupTwoStepVerificationInitialState {
case automatic
case createPassword
case updatePassword(current: String, hasRecoveryEmail: Bool, hasSecureValues: Bool)
case addEmail(hadRecoveryEmail: Bool, hasSecureValues: Bool, password: String)
case confirmEmail(password: String?, hasSecureValues: Bool, pattern: String, codeLength: Int32?)
}
enum SetupTwoStepVerificationStateKind: Int32 {
case enterPassword
case confirmPassword
case enterHint
case enterEmail
case confirmEmail
}
private enum CreatePasswordMode: Equatable {
case create
case update(current: String, hasRecoveryEmail: Bool, hasSecureValues: Bool)
}
private enum EnterEmailState: Equatable {
case create(password: String, hint: String)
case add(hadRecoveryEmail: Bool, hasSecureValues: Bool, password: String)
}
private enum ConfirmEmailState: Equatable {
case create(password: String, hint: String, email: String)
case add(password: String, hadRecoveryEmail: Bool, hasSecureValues: Bool, email: String)
case confirm(password: String?, hasSecureValues: Bool, pattern: String, codeLength: Int32?)
}
private enum SetupTwoStepVerificationState: Equatable {
case enterPassword(mode: CreatePasswordMode, password: String)
case confirmPassword(mode: CreatePasswordMode, password: String, confirmation: String)
case enterHint(mode: CreatePasswordMode, password: String, hint: String)
case enterEmail(state: EnterEmailState, email: String)
case confirmEmail(state: ConfirmEmailState, pattern: String, codeLength: Int32?, code: String)
var kind: SetupTwoStepVerificationStateKind {
switch self {
case .enterPassword:
return .enterPassword
case .confirmPassword:
return .confirmPassword
case .enterHint:
return .enterHint
case .enterEmail:
return .enterEmail
case .confirmEmail:
return .confirmEmail
}
}
mutating func updateInputText(_ text: String) {
switch self {
case let .enterPassword(mode, _):
self = .enterPassword(mode: mode, password: text)
case let .confirmPassword(mode, password, _):
self = .confirmPassword(mode: mode, password: password, confirmation: text)
case let .enterHint(mode, password, _):
self = .enterHint(mode: mode, password: password, hint: text)
case let .enterEmail(state, _):
self = .enterEmail(state: state, email: text)
case let .confirmEmail(state, pattern, codeLength, _):
self = .confirmEmail(state: state, pattern: pattern, codeLength: codeLength, code: text)
}
}
}
extension SetupTwoStepVerificationState {
init?(initialState: SetupTwoStepVerificationInitialState) {
switch initialState {
case .automatic:
return nil
case .createPassword:
self = .enterPassword(mode: .create, password: "")
case let .updatePassword(current, hasRecoveryEmail, hasSecureValues):
self = .enterPassword(mode: .update(current: current, hasRecoveryEmail: hasRecoveryEmail, hasSecureValues: hasSecureValues), password: "")
case let .addEmail(hadRecoveryEmail, hasSecureValues, password):
self = .enterEmail(state: .add(hadRecoveryEmail: hadRecoveryEmail, hasSecureValues: hasSecureValues, password: password), email: "")
case let .confirmEmail(password, hasSecureValues, pattern, codeLength):
self = .confirmEmail(state: .confirm(password: password, hasSecureValues: hasSecureValues, pattern: pattern, codeLength: codeLength), pattern: pattern, codeLength: codeLength, code: "")
}
}
}
private struct SetupTwoStepVerificationControllerDataState: Equatable {
var activity: Bool
var state: SetupTwoStepVerificationState?
}
private struct SetupTwoStepVerificationControllerLayoutState: Equatable {
let layout: ContainerViewLayout
let navigationHeight: CGFloat
}
private struct SetupTwoStepVerificationControllerInnerState: Equatable {
var layout: SetupTwoStepVerificationControllerLayoutState?
var data: SetupTwoStepVerificationControllerDataState
}
private struct SetupTwoStepVerificationControllerState: Equatable {
var layout: SetupTwoStepVerificationControllerLayoutState
var data: SetupTwoStepVerificationControllerDataState
}
extension SetupTwoStepVerificationControllerState {
init?(_ state: SetupTwoStepVerificationControllerInnerState) {
guard let layout = state.layout else {
return nil
}
self.init(layout: layout, data: state.data)
}
}
enum SetupTwoStepVerificationNextAction: Equatable {
case none
case activity
case button(title: String, isEnabled: Bool)
}
public enum SetupTwoStepVerificationStateUpdate {
case noPassword
case awaitingEmailConfirmation(password: String, pattern: String, codeLength: Int32?)
case passwordSet(password: String?, hasRecoveryEmail: Bool, hasSecureValues: Bool)
case pendingPasswordReset
}
final class SetupTwoStepVerificationControllerNode: ViewControllerTracingNode {
private let context: AccountContext
private var presentationData: PresentationData
private let updateBackAction: (Bool) -> Void
private let updateNextAction: (SetupTwoStepVerificationNextAction) -> Void
private let stateUpdated: (SetupTwoStepVerificationStateUpdate, Bool) -> Void
private let present: (ViewController, Any?) -> Void
private let dismiss: () -> Void
private var innerState: SetupTwoStepVerificationControllerInnerState
private let activityIndicator: ActivityIndicator
private var contentNode: SetupTwoStepVerificationContentNode?
private let actionDisposable = MetaDisposable()
init(context: AccountContext, updateBackAction: @escaping (Bool) -> Void, updateNextAction: @escaping (SetupTwoStepVerificationNextAction) -> Void, stateUpdated: @escaping (SetupTwoStepVerificationStateUpdate, Bool) -> Void, present: @escaping (ViewController, Any?) -> Void, dismiss: @escaping () -> Void, initialState: SetupTwoStepVerificationInitialState) {
self.context = context
self.updateBackAction = updateBackAction
self.updateNextAction = updateNextAction
self.stateUpdated = stateUpdated
self.present = present
self.dismiss = dismiss
self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
self.innerState = SetupTwoStepVerificationControllerInnerState(layout: nil, data: SetupTwoStepVerificationControllerDataState(activity: false, state: SetupTwoStepVerificationState(initialState: initialState)))
self.activityIndicator = ActivityIndicator(type: .custom(self.presentationData.theme.list.itemAccentColor, 22.0, 2.0, false))
super.init()
self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
self.processStateUpdated()
if self.innerState.data.state == nil {
self.actionDisposable.set((self.context.engine.auth.twoStepAuthData()
|> deliverOnMainQueue).start(next: { [weak self] data in
guard let strongSelf = self else {
return
}
if data.currentPasswordDerivation != nil {
strongSelf.stateUpdated(.passwordSet(password: nil, hasRecoveryEmail: data.hasRecovery, hasSecureValues: data.hasSecretValues), true)
} else {
strongSelf.updateState({ state in
var state = state
if let unconfirmedEmailPattern = data.unconfirmedEmailPattern {
state.data.state = .confirmEmail(state: .confirm(password: nil, hasSecureValues: data.hasSecretValues, pattern: unconfirmedEmailPattern, codeLength: nil), pattern: unconfirmedEmailPattern, codeLength: nil, code: "")
} else {
state.data.state = .enterPassword(mode: .create, password: "")
}
return state
}, transition: .animated(duration: 0.3, curve: .easeInOut))
}
}))
}
}
deinit {
self.actionDisposable.dispose()
}
func updatePresentationData(_ presentationData: PresentationData) {
self.presentationData = presentationData
self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
self.contentNode?.updatePresentationData(presentationData)
}
func animateIn(completion: (() -> Void)? = nil) {
self.layer.animatePosition(from: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), to: self.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in
completion?()
})
}
func animateOut(completion: (() -> Void)? = nil) {
self.layer.animatePosition(from: self.layer.position, to: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { _ in
completion?()
})
}
private func updateState(_ f: (SetupTwoStepVerificationControllerInnerState) -> SetupTwoStepVerificationControllerInnerState, transition: ContainedViewLayoutTransition) {
let updatedState = f(self.innerState)
if updatedState != self.innerState {
self.innerState = updatedState
self.processStateUpdated()
if let state = SetupTwoStepVerificationControllerState(updatedState) {
self.transition(state: state, transition: transition)
}
}
}
private func processStateUpdated() {
var backAction = false
let nextAction: SetupTwoStepVerificationNextAction
if self.innerState.data.activity {
nextAction = .activity
} else if let state = self.innerState.data.state {
switch state {
case let .enterPassword(_, password):
nextAction = .button(title: self.presentationData.strings.Common_Next, isEnabled: !password.isEmpty)
case let .confirmPassword(_, _, confirmation):
nextAction = .button(title: self.presentationData.strings.Common_Next, isEnabled: !confirmation.isEmpty)
backAction = true
case let .enterHint(_, _, hint):
nextAction = .button(title: hint.isEmpty ? self.presentationData.strings.TwoStepAuth_EmailSkip : self.presentationData.strings.Common_Next, isEnabled: true)
backAction = true
case let .enterEmail(enterState, email):
switch enterState {
case .create:
nextAction = .button(title: email.isEmpty ? self.presentationData.strings.TwoStepAuth_EmailSkip : self.presentationData.strings.Common_Next, isEnabled: true)
case .add:
nextAction = .button(title: self.presentationData.strings.Common_Next, isEnabled: !email.isEmpty)
}
case let .confirmEmail(_, _, _, code):
nextAction = .button(title: self.presentationData.strings.Common_Next, isEnabled: !code.isEmpty)
}
} else {
nextAction = .none
}
self.updateBackAction(backAction)
self.updateNextAction(nextAction)
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.updateState({ state in
var state = state
state.layout = SetupTwoStepVerificationControllerLayoutState(layout: layout, navigationHeight: navigationBarHeight)
return state
}, transition: transition)
let indicatorSize = CGSize(width: 22.0, height: 22.0)
self.activityIndicator.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - indicatorSize.width) / 2.0), y: floor((layout.size.height - indicatorSize.height) / 2.0)), size: indicatorSize)
}
private func transition(state: SetupTwoStepVerificationControllerState, transition: ContainedViewLayoutTransition) {
var insets = state.layout.layout.insets(options: [.statusBar])
let visibleInsets = state.layout.layout.insets(options: [.statusBar, .input])
if let inputHeight = state.layout.layout.inputHeight {
insets.bottom += max(inputHeight, state.layout.layout.standardInputHeight)
}
let contentFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: state.layout.layout.size.width, height: state.layout.layout.size.height))
if state.data.state?.kind != self.contentNode?.kind {
if let dataState = state.data.state {
let title: String
let subtitle: String
let inputType: SetupTwoStepVerificationInputType
let inputPlaceholder: String
let inputText: String
let isPassword: Bool
var leftAction: SetupTwoStepVerificationContentAction?
var rightAction: SetupTwoStepVerificationContentAction?
switch dataState {
case let .enterPassword(mode, password):
switch mode {
case .create:
title = self.presentationData.strings.TwoStepAuth_SetupPasswordTitle
subtitle = self.presentationData.strings.TwoStepAuth_SetupPasswordDescription
case .update:
title = self.presentationData.strings.TwoStepAuth_ChangePassword
subtitle = self.presentationData.strings.TwoStepAuth_ChangePasswordDescription
}
inputType = .password
inputPlaceholder = self.presentationData.strings.LoginPassword_PasswordPlaceholder
inputText = password
isPassword = true
case let .confirmPassword(_, _, confirmation):
title = self.presentationData.strings.TwoStepAuth_ReEnterPasswordTitle
subtitle = self.presentationData.strings.TwoStepAuth_ReEnterPasswordDescription
inputType = .password
inputPlaceholder = self.presentationData.strings.LoginPassword_PasswordPlaceholder
inputText = confirmation
isPassword = true
case let .enterHint(_, _, hint):
title = self.presentationData.strings.TwoStepAuth_AddHintTitle
subtitle = self.presentationData.strings.TwoStepAuth_AddHintDescription
inputType = .text
inputPlaceholder = self.presentationData.strings.TwoStepAuth_HintPlaceholder
inputText = hint
isPassword = false
case let .enterEmail(enterState, email):
title = self.presentationData.strings.TwoStepAuth_RecoveryEmailTitle
switch enterState {
case let .add(hadRecoveryEmail, _, _) where hadRecoveryEmail:
subtitle = self.presentationData.strings.TwoStepAuth_RecoveryEmailChangeDescription
default:
subtitle = self.presentationData.strings.TwoStepAuth_RecoveryEmailAddDescription
}
inputType = .email
inputPlaceholder = self.presentationData.strings.TwoStepAuth_EmailPlaceholder
inputText = email
isPassword = false
case let .confirmEmail(confirmState, _, _, code):
title = self.presentationData.strings.TwoStepAuth_RecoveryEmailTitle
let emailPattern: String
switch confirmState {
case let .create(password, hint, email):
emailPattern = email
leftAction = SetupTwoStepVerificationContentAction(title: self.presentationData.strings.TwoStepAuth_ChangeEmail, action: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.updateState({ state in
var state = state
state.data.activity = true
return state
}, transition: .animated(duration: 0.5, curve: .spring))
strongSelf.actionDisposable.set((strongSelf.context.engine.auth.updateTwoStepVerificationPassword(currentPassword: nil, updatedPassword: .none)
|> deliverOnMainQueue).start(next: { _ in
guard let strongSelf = self else {
return
}
strongSelf.updateState({ state in
var state = state
state.data.activity = false
state.data.state = .enterEmail(state: .create(password: password, hint: hint), email: "")
return state
}, transition: .animated(duration: 0.5, curve: .spring))
strongSelf.stateUpdated(.noPassword, false)
}, error: { _ in
guard let strongSelf = self else {
return
}
strongSelf.present(textAlertController(sharedContext: strongSelf.context.sharedContext, title: nil, text: strongSelf.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), nil)
strongSelf.updateState({ state in
var state = state
state.data.activity = false
return state
}, transition: .animated(duration: 0.5, curve: .spring))
}))
})
case let .add(password, hadRecoveryEmail, hasSecureValues, email):
emailPattern = email
leftAction = SetupTwoStepVerificationContentAction(title: self.presentationData.strings.TwoStepAuth_ChangeEmail, action: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.updateState({ state in
var state = state
state.data.state = .enterEmail(state: .add(hadRecoveryEmail: hadRecoveryEmail, hasSecureValues: hasSecureValues, password: password), email: "")
return state
}, transition: .animated(duration: 0.5, curve: .spring))
})
case let .confirm(_, _, pattern, _):
emailPattern = pattern
}
subtitle = self.presentationData.strings.TwoStepAuth_ConfirmEmailDescription(emailPattern).string
inputType = .code
inputPlaceholder = self.presentationData.strings.TwoStepAuth_ConfirmEmailCodePlaceholder
inputText = code
isPassword = true
rightAction = SetupTwoStepVerificationContentAction(title: self.presentationData.strings.TwoStepAuth_ConfirmEmailResendCode, action: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.updateState({ state in
var state = state
state.data.activity = true
return state
}, transition: .animated(duration: 0.5, curve: .spring))
strongSelf.actionDisposable.set((strongSelf.context.engine.auth.resendTwoStepRecoveryEmail()
|> deliverOnMainQueue).start(error: { error in
guard let strongSelf = self else {
return
}
let text: String
switch error {
case .flood:
text = strongSelf.presentationData.strings.TwoStepAuth_FloodError
case .generic:
text = strongSelf.presentationData.strings.Login_UnknownError
}
strongSelf.present(textAlertController(sharedContext: strongSelf.context.sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), nil)
strongSelf.updateState({ state in
var state = state
state.data.activity = false
return state
}, transition: .animated(duration: 0.5, curve: .spring))
}, completed: {
guard let strongSelf = self else {
return
}
strongSelf.updateState({ state in
var state = state
state.data.activity = false
return state
}, transition: .animated(duration: 0.5, curve: .spring))
}))
})
}
let contentNode = SetupTwoStepVerificationContentNode(theme: self.presentationData.theme, kind: dataState.kind, title: title, subtitle: subtitle, inputType: inputType, placeholder: inputPlaceholder, text: inputText, isPassword: isPassword, textUpdated: { [weak self] text in
guard let strongSelf = self else {
return
}
var inplicitelyActivateNextAction = false
if case let .confirmEmail(_, _, codeLength?, code)? = strongSelf.innerState.data.state, code.count != codeLength, text.count == codeLength {
inplicitelyActivateNextAction = true
}
strongSelf.updateState({ state in
var state = state
state.data.state?.updateInputText(text)
return state
}, transition: .immediate)
if inplicitelyActivateNextAction {
strongSelf.activateNextAction()
}
}, returnPressed: { [weak self] in
self?.activateNextAction()
}, leftAction: leftAction, rightAction: rightAction)
self.insertSubnode(contentNode, at: 0)
contentNode.updateIsEnabled(!state.data.activity)
contentNode.updateLayout(size: contentFrame.size, insets: insets, visibleInsets: visibleInsets, transition: .immediate)
contentNode.frame = contentFrame
contentNode.activate()
if let currentContentNode = self.contentNode {
if currentContentNode.kind.rawValue < contentNode.kind.rawValue {
transition.updatePosition(node: currentContentNode, position: CGPoint(x: -contentFrame.size.width / 2.0, y: contentFrame.midY), completion: { [weak currentContentNode] _ in
currentContentNode?.removeFromSupernode()
})
transition.animateHorizontalOffsetAdditive(node: contentNode, offset: -contentFrame.width)
} else {
transition.updatePosition(node: currentContentNode, position: CGPoint(x: contentFrame.size.width + contentFrame.size.width / 2.0, y: contentFrame.midY), completion: { [weak currentContentNode] _ in
currentContentNode?.removeFromSupernode()
})
transition.animateHorizontalOffsetAdditive(node: contentNode, offset: contentFrame.width)
}
} else if transition.isAnimated {
contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
}
if self.activityIndicator.supernode != nil {
transition.updateAlpha(node: self.activityIndicator, alpha: 0.0, completion: { [weak self] _ in
self?.activityIndicator.removeFromSupernode()
})
}
self.contentNode = contentNode
} else if let currentContentNode = self.contentNode {
transition.updateAlpha(node: currentContentNode, alpha: 0.0, completion: { [weak currentContentNode] _ in
currentContentNode?.removeFromSupernode()
})
if self.activityIndicator.supernode == nil {
self.addSubnode(self.activityIndicator)
transition.updateAlpha(node: self.activityIndicator, alpha: 1.0)
}
self.contentNode = nil
}
} else if let contentNode = self.contentNode {
contentNode.updateIsEnabled(!state.data.activity)
transition.updateFrame(node: contentNode, frame: contentFrame)
contentNode.updateLayout(size: contentFrame.size, insets: insets, visibleInsets: visibleInsets, transition: transition)
}
}
func activateBackAction() {
if self.innerState.data.activity {
return
}
self.updateState({ state in
var state = state
if let dataState = state.data.state {
switch dataState {
case let .confirmPassword(mode, _, _):
state.data.state = .enterPassword(mode: mode, password: "")
case let .enterHint(mode, _, _):
state.data.state = .enterPassword(mode: mode, password: "")
default:
break
}
}
return state
}, transition: .animated(duration: 0.5, curve: .spring))
}
func activateNextAction() {
if self.innerState.data.activity {
return
}
let continueImpl: () -> Void = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.updateState({ state in
guard let dataState = state.data.state else {
return state
}
var state = state
switch dataState {
case let .enterPassword(mode, password):
state.data.state = .confirmPassword(mode: mode, password: password, confirmation: "")
case let .confirmPassword(mode, password, confirmation):
if password == confirmation {
state.data.state = .enterHint(mode: mode, password: password, hint: "")
} else {
strongSelf.present(textAlertController(sharedContext: strongSelf.context.sharedContext, title: nil, text: strongSelf.presentationData.strings.TwoStepAuth_SetupPasswordConfirmFailed, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), nil)
}
case let .enterHint(mode, password, hint):
switch mode {
case .create:
state.data.state = .enterEmail(state: .create(password: password, hint: hint), email: "")
case let .update(current, hasRecoveryEmail, hasSecureValues):
state.data.activity = true
strongSelf.actionDisposable.set((strongSelf.context.engine.auth.updateTwoStepVerificationPassword(currentPassword: current, updatedPassword: .password(password: password, hint: hint, email: nil))
|> deliverOnMainQueue).start(next: { result in
guard let strongSelf = self else {
return
}
strongSelf.updateState({ state in
var state = state
state.data.activity = false
switch result {
case let .password(password, pendingEmail):
if let pendingEmail = pendingEmail {
strongSelf.stateUpdated(.awaitingEmailConfirmation(password: password, pattern: pendingEmail.pattern, codeLength: pendingEmail.codeLength), true)
} else {
strongSelf.stateUpdated(.passwordSet(password: password, hasRecoveryEmail: hasRecoveryEmail, hasSecureValues: hasSecureValues), true)
}
case .none:
strongSelf.dismiss()
}
return state
}, transition: .animated(duration: 0.5, curve: .spring))
}, error: { error in
guard let strongSelf = self else {
return
}
strongSelf.present(textAlertController(sharedContext: strongSelf.context.sharedContext, title: nil, text: strongSelf.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), nil)
strongSelf.updateState({ state in
var state = state
state.data.activity = false
return state
}, transition: .animated(duration: 0.5, curve: .spring))
}))
}
case let .enterEmail(enterState, email):
state.data.activity = true
switch enterState {
case let .create(password, hint):
strongSelf.actionDisposable.set((strongSelf.context.engine.auth.updateTwoStepVerificationPassword(currentPassword: nil, updatedPassword: .password(password: password, hint: hint, email: email))
|> deliverOnMainQueue).start(next: { result in
guard let strongSelf = self else {
return
}
strongSelf.updateState({ state in
var state = state
switch result {
case let .password(password, pendingEmail):
if let pendingEmail = pendingEmail {
state.data.activity = false
state.data.state = .confirmEmail(state: .create(password: password, hint: hint, email: email), pattern: pendingEmail.pattern, codeLength: pendingEmail.codeLength, code: "")
strongSelf.stateUpdated(.awaitingEmailConfirmation(password: password, pattern: pendingEmail.pattern, codeLength: pendingEmail.codeLength), false)
} else {
strongSelf.stateUpdated(.passwordSet(password: password, hasRecoveryEmail: false, hasSecureValues: false), true)
}
case .none:
break
}
return state
}, transition: .animated(duration: 0.5, curve: .spring))
}, error: { error in
guard let strongSelf = self else {
return
}
let text: String
switch error {
case .invalidEmail:
text = strongSelf.presentationData.strings.TwoStepAuth_EmailInvalid
case .generic:
text = strongSelf.presentationData.strings.Login_UnknownError
}
strongSelf.present(textAlertController(sharedContext: strongSelf.context.sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), nil)
strongSelf.updateState({ state in
var state = state
state.data.activity = false
if case .invalidEmail = error {
state.data.state = .enterEmail(state: .create(password: password, hint: hint), email: "")
}
return state
}, transition: .animated(duration: 0.5, curve: .spring))
}))
case let .add(hadRecoveryEmail, hasSecureValues, password):
strongSelf.updateState({ state in
var state = state
state.data.activity = true
return state
}, transition: .animated(duration: 0.5, curve: .spring))
strongSelf.actionDisposable.set((strongSelf.context.engine.auth.updateTwoStepVerificationEmail(currentPassword: password, updatedEmail: email)
|> deliverOnMainQueue).start(next: { result in
guard let strongSelf = self else {
return
}
strongSelf.updateState({ state in
var state = state
state.data.activity = false
switch result {
case .none:
assertionFailure()
break
case let .password(password, pendingEmail):
if let pendingEmail = pendingEmail {
state.data.state = .confirmEmail(state: .add(password: password, hadRecoveryEmail: hadRecoveryEmail, hasSecureValues: hasSecureValues, email: email), pattern: pendingEmail.pattern, codeLength: pendingEmail.codeLength, code: "")
strongSelf.stateUpdated(.awaitingEmailConfirmation(password: password, pattern: pendingEmail.pattern, codeLength: pendingEmail.codeLength), false)
} else {
strongSelf.stateUpdated(.passwordSet(password: password, hasRecoveryEmail: true, hasSecureValues: hasSecureValues), true)
}
}
return state
}, transition: .animated(duration: 0.5, curve: .spring))
}, error: { _ in
guard let strongSelf = self else {
return
}
strongSelf.present(textAlertController(sharedContext: strongSelf.context.sharedContext, title: nil, text: strongSelf.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), nil)
strongSelf.updateState({ state in
var state = state
state.data.activity = false
return state
}, transition: .animated(duration: 0.5, curve: .spring))
}))
}
case let .confirmEmail(confirmState, _, _, code):
state.data.activity = true
strongSelf.actionDisposable.set((strongSelf.context.engine.auth.confirmTwoStepRecoveryEmail(code: code)
|> deliverOnMainQueue).start(error: { error in
guard let strongSelf = self else {
return
}
let text: String
switch error {
case .invalidEmail:
text = strongSelf.presentationData.strings.TwoStepAuth_EmailInvalid
case .invalidCode:
text = strongSelf.presentationData.strings.Login_InvalidCodeError
strongSelf.contentNode?.dataEntryError()
case .expired:
text = strongSelf.presentationData.strings.TwoStepAuth_EmailCodeExpired
case .flood:
text = strongSelf.presentationData.strings.TwoStepAuth_FloodError
case .generic:
text = strongSelf.presentationData.strings.Login_UnknownError
}
strongSelf.present(textAlertController(sharedContext: strongSelf.context.sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), nil)
strongSelf.updateState({ state in
var state = state
state.data.activity = false
return state
}, transition: .animated(duration: 0.5, curve: .spring))
}, completed: {
guard let strongSelf = self else {
return
}
switch confirmState {
case let .create(password, _, _):
strongSelf.stateUpdated(.passwordSet(password: password, hasRecoveryEmail: true, hasSecureValues: false), true)
case let .add(password, _, hasSecureValues, email):
strongSelf.stateUpdated(.passwordSet(password: password, hasRecoveryEmail: !email.isEmpty, hasSecureValues: hasSecureValues), true)
case let .confirm(password, hasSecureValues, _, _):
strongSelf.stateUpdated(.passwordSet(password: password, hasRecoveryEmail: true, hasSecureValues: hasSecureValues), true)
}
}))
}
return state
}, transition: .animated(duration: 0.5, curve: .spring))
}
if case let .enterEmail(enterEmailState, enterEmailEmail)? = self.innerState.data.state, case .create = enterEmailState, enterEmailEmail.isEmpty {
self.present(textAlertController(sharedContext: self.context.sharedContext, title: nil, text: self.presentationData.strings.TwoStepAuth_EmailSkipAlert, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .destructiveAction, title: self.presentationData.strings.TwoStepAuth_EmailSkip, action: {
continueImpl()
})]), nil)
} else {
continueImpl()
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,334 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SolidRoundedButtonNode
import SwiftSignalKit
import OverlayStatusController
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import AccountContext
import TelegramPresentationData
import PresentationDataUtils
import TelegramCore
public enum TwoFactorAuthSplashMode {
public struct Intro {
public var title: String
public var text: String
public var actionText: String
public var doneText: String
public var phoneNumber: String?
public init(
title: String,
text: String,
actionText: String,
doneText: String,
phoneNumber: String?
) {
self.title = title
self.text = text
self.actionText = actionText
self.doneText = doneText
self.phoneNumber = phoneNumber
}
}
case intro(Intro)
case done(doneText: String)
case recoveryDone(recoveredAccountData: RecoveredAccountData?, syncContacts: Bool, isPasswordSet: Bool)
case remember
}
public final class TwoFactorAuthSplashScreen: ViewController {
private let sharedContext: SharedAccountContext
private let engine: SomeTelegramEngine
private var presentationData: PresentationData
private var mode: TwoFactorAuthSplashMode
public var dismissConfirmation: ((@escaping () -> Void) -> Bool)?
public init(sharedContext: SharedAccountContext, engine: SomeTelegramEngine, mode: TwoFactorAuthSplashMode, presentation: ViewControllerNavigationPresentation = .modalInLargeLayout) {
self.sharedContext = sharedContext
self.engine = engine
self.mode = mode
self.presentationData = self.sharedContext.currentPresentationData.with { $0 }
let defaultTheme = NavigationBarTheme(rootControllerTheme: self.presentationData.theme)
let navigationBarTheme = NavigationBarTheme(buttonColor: defaultTheme.buttonColor, disabledButtonColor: defaultTheme.disabledButtonColor, primaryTextColor: defaultTheme.primaryTextColor, backgroundColor: .clear, enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: defaultTheme.badgeBackgroundColor, badgeStrokeColor: defaultTheme.badgeStrokeColor, badgeTextColor: defaultTheme.badgeTextColor)
super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: navigationBarTheme, strings: NavigationBarStrings(back: self.presentationData.strings.Common_Back, close: self.presentationData.strings.Common_Close)))
self.navigationPresentation = presentation
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
self.navigationBar?.intrinsicCanTransitionInline = false
let hasBackButton: Bool
switch mode {
case .done, .remember:
hasBackButton = false
default:
hasBackButton = true
}
if hasBackButton {
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
} else {
self.navigationItem.leftBarButtonItem = UIBarButtonItem(customDisplayNode: ASDisplayNode())
}
self.attemptNavigation = { [weak self] f in
guard let strongSelf = self, let dismissConfirmation = strongSelf.dismissConfirmation else {
return true
}
return dismissConfirmation(f)
}
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
override public func loadDisplayNode() {
self.displayNode = TwoFactorAuthSplashScreenNode(sharedContext: self.sharedContext, presentationData: self.presentationData, mode: self.mode, action: { [weak self] in
guard let strongSelf = self else {
return
}
switch strongSelf.mode {
case let .intro(intro):
strongSelf.push(TwoFactorDataInputScreen(sharedContext: strongSelf.sharedContext, engine: strongSelf.engine, mode: .password(phoneNumber: intro.phoneNumber, doneText: intro.doneText), stateUpdated: { _ in
}, presentation: strongSelf.navigationPresentation))
case .done, .remember:
guard let navigationController = strongSelf.navigationController as? NavigationController else {
return
}
navigationController.filterController(strongSelf, animated: true)
case let .recoveryDone(recoveredAccountData, syncContacts, _):
guard let navigationController = strongSelf.navigationController as? NavigationController else {
return
}
switch strongSelf.engine {
case let .unauthorized(engine):
if let recoveredAccountData = recoveredAccountData {
let _ = loginWithRecoveredAccountData(accountManager: strongSelf.sharedContext.accountManager, account: engine.account, recoveredAccountData: recoveredAccountData, syncContacts: syncContacts).start()
}
case .authorized:
navigationController.filterController(strongSelf, animated: true)
}
}
})
self.displayNodeDidLoad()
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
(self.displayNode as! TwoFactorAuthSplashScreenNode).containerLayoutUpdated(layout: layout, navigationHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
}
}
private final class TwoFactorAuthSplashScreenNode: ViewControllerTracingNode {
private var presentationData: PresentationData
private let mode: TwoFactorAuthSplashMode
private var animationSize: CGSize = CGSize()
private var animationOffset: CGPoint = CGPoint()
private let animationNode: AnimatedStickerNode
private let titleNode: ImmediateTextNode
private let textNodes: [ImmediateTextNode]
private let textArrowNodes: [ASImageNode]
let buttonNode: SolidRoundedButtonNode
var inProgress: Bool = false {
didSet {
self.buttonNode.isUserInteractionEnabled = !self.inProgress
self.buttonNode.alpha = self.inProgress ? 0.6 : 1.0
}
}
init(sharedContext: SharedAccountContext, presentationData: PresentationData, mode: TwoFactorAuthSplashMode, action: @escaping () -> Void) {
self.presentationData = presentationData
self.mode = mode
self.animationNode = DefaultAnimatedStickerNodeImpl()
let title: String
let texts: [NSAttributedString]
let buttonText: String
let textFont = Font.regular(16.0)
let textColor = self.presentationData.theme.list.itemPrimaryTextColor
switch mode {
case let .intro(intro):
title = intro.title
texts = [NSAttributedString(string: intro.text, font: textFont, textColor: textColor)]
buttonText = intro.actionText
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "TwoFactorSetupIntro"), width: 248, height: 248, playbackMode: .once, mode: .direct(cachePathPrefix: nil))
self.animationSize = CGSize(width: 124.0, height: 124.0)
self.animationNode.visibility = true
case let .done(doneText):
title = self.presentationData.strings.TwoFactorSetup_Done_Title
texts = [NSAttributedString(string: self.presentationData.strings.TwoFactorSetup_Done_Text, font: textFont, textColor: textColor)]
buttonText = doneText
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "TwoFactorSetupDone"), width: 248, height: 248, playbackMode: .loop, mode: .direct(cachePathPrefix: nil))
self.animationSize = CGSize(width: 124.0, height: 124.0)
self.animationNode.visibility = true
case let .recoveryDone(_, _, isPasswordSet):
title = isPasswordSet ? self.presentationData.strings.TwoFactorSetup_ResetDone_Title : self.presentationData.strings.TwoFactorSetup_ResetDone_TitleNoPassword
let rawText = isPasswordSet ? self.presentationData.strings.TwoFactorSetup_ResetDone_Text : self.presentationData.strings.TwoFactorSetup_ResetDone_TextNoPassword
var splitTexts: [String] = [""]
var index = rawText.startIndex
while index != rawText.endIndex {
let c = rawText[index]
if c == ">" {
splitTexts.append("")
} else {
splitTexts[splitTexts.count - 1].append(c)
}
index = rawText.index(after: index)
}
texts = splitTexts.map { NSAttributedString(string: $0, font: textFont, textColor: textColor) }
buttonText = self.presentationData.strings.TwoFactorSetup_ResetDone_Action
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: isPasswordSet ? "TwoFactorSetupDone" : "TwoFactorRemovePasswordDone"), width: 248, height: 248, playbackMode: isPasswordSet ? .loop : .once, mode: .direct(cachePathPrefix: nil))
self.animationSize = CGSize(width: 124.0, height: 124.0)
self.animationNode.visibility = true
case .remember:
title = self.presentationData.strings.TwoFactorRemember_Done_Title
texts = [NSAttributedString(string: self.presentationData.strings.TwoFactorRemember_Done_Text, font: textFont, textColor: textColor)]
buttonText = self.presentationData.strings.TwoFactorRemember_Done_Action
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "TwoFactorSetupRememberSuccess"), width: 248, height: 248, playbackMode: .loop, mode: .direct(cachePathPrefix: nil))
self.animationSize = CGSize(width: 124.0, height: 124.0)
self.animationNode.visibility = true
}
self.titleNode = ImmediateTextNode()
self.titleNode.displaysAsynchronously = false
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.bold(32.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
self.titleNode.maximumNumberOfLines = 0
self.titleNode.textAlignment = .center
self.textNodes = texts.map { text in
let textNode = ImmediateTextNode()
textNode.displaysAsynchronously = false
textNode.attributedText = text
textNode.maximumNumberOfLines = 0
textNode.lineSpacing = 0.1
textNode.textAlignment = .center
return textNode
}
let arrowImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Search/DownButton"), color: presentationData.theme.list.itemPrimaryTextColor)
self.textArrowNodes = (0 ..< self.textNodes.count - 1).map { _ in
let iconNode = ASImageNode()
iconNode.image = arrowImage
iconNode.alpha = 0.34
return iconNode
}
self.buttonNode = SolidRoundedButtonNode(title: buttonText, theme: SolidRoundedButtonTheme(backgroundColor: self.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: self.presentationData.theme.list.itemCheckColors.foregroundColor), height: 50.0, cornerRadius: 11.0, isShimmering: false)
self.buttonNode.isHidden = buttonText.isEmpty
super.init()
self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
self.addSubnode(self.animationNode)
self.addSubnode(self.titleNode)
self.textNodes.forEach(self.addSubnode)
self.textArrowNodes.forEach(self.addSubnode)
self.addSubnode(self.buttonNode)
self.buttonNode.pressed = {
action()
}
}
override func didLoad() {
super.didLoad()
}
func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) {
let sideInset: CGFloat = 32.0
let buttonSideInset: CGFloat = 48.0
let iconSpacing: CGFloat = 8.0
let titleSpacing: CGFloat = 19.0
let buttonHeight: CGFloat = 50.0
let iconSize: CGSize = self.animationSize
var iconOffset = CGPoint()
switch self.mode {
case .done:
iconOffset.x = 10.0
default:
break
}
let titleSize = self.titleNode.updateLayout(CGSize(width: layout.size.width - sideInset * 2.0, height: layout.size.height))
let textSizes = self.textNodes.map {
$0.updateLayout(CGSize(width: layout.size.width - sideInset * 2.0, height: layout.size.height))
}
var combinedTextHeight: CGFloat = 0.0
let textSpacing: CGFloat = 32.0
for textSize in textSizes {
combinedTextHeight += textSize.height
}
combinedTextHeight += CGFloat(max(0, textSizes.count - 1)) * textSpacing
let contentHeight = iconSize.height + iconSpacing + titleSize.height + titleSpacing + combinedTextHeight
var contentVerticalOrigin = floor((layout.size.height - contentHeight - iconSize.height / 2.0) / 2.0)
let minimalBottomInset: CGFloat = 60.0
let bottomInset = layout.intrinsicInsets.bottom + minimalBottomInset
let buttonWidth = layout.size.width - buttonSideInset * 2.0
let buttonFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - buttonWidth) / 2.0), y: layout.size.height - bottomInset - buttonHeight), size: CGSize(width: buttonWidth, height: buttonHeight))
transition.updateFrame(node: self.buttonNode, frame: buttonFrame)
let _ = self.buttonNode.updateLayout(width: buttonFrame.width, transition: transition)
let maxContentVerticalOrigin = buttonFrame.minY - 12.0 - contentHeight
contentVerticalOrigin = min(contentVerticalOrigin, maxContentVerticalOrigin)
let iconFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - iconSize.width) / 2.0) + self.animationOffset.x, y: contentVerticalOrigin + self.animationOffset.y), size: iconSize).offsetBy(dx: iconOffset.x, dy: iconOffset.y)
self.animationNode.updateLayout(size: iconFrame.size)
transition.updateFrameAdditive(node: self.animationNode, frame: iconFrame)
let titleFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - titleSize.width) / 2.0), y: iconFrame.maxY + iconSpacing), size: titleSize)
transition.updateFrameAdditive(node: self.titleNode, frame: titleFrame)
var nextTextOrigin: CGFloat = titleFrame.maxY + titleSpacing
for i in 0 ..< self.textNodes.count {
let textFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - textSizes[i].width) / 2.0), y: nextTextOrigin), size: textSizes[i])
transition.updateFrameAdditive(node: self.textNodes[i], frame: textFrame)
if i != 0 {
if let image = self.textArrowNodes[i - 1].image {
let scaledImageSize = CGSize(width: floor(image.size.width * 0.7), height: floor(image.size.height * 0.7))
self.textArrowNodes[i - 1].frame = CGRect(origin: CGPoint(x: floor((layout.size.width - scaledImageSize.width) / 2.0), y: nextTextOrigin - textSpacing + floor((textSpacing - scaledImageSize.height) / 2.0)), size: scaledImageSize)
}
}
nextTextOrigin = textFrame.maxY + textSpacing
}
}
}