Files
2024-06-28 14:29:41 -03:00

424 lines
15 KiB
Swift

//
// Auth_CodeEntry.swift
// Telegram
//
// Created by Mike Renoir on 15.02.2022.
// Copyright © 2022 Telegram. All rights reserved.
//
import Foundation
import TGUIKit
import TelegramCore
import AppKit
import KeyboardKey
import SwiftSignalKit
import TelegramMedia
private final class Auth_CodeEntryHeaderView : View {
enum IconType {
case phone
case desktop
case pirate
var header: String {
switch self {
case .phone:
return strings().loginNewCodeEnterSms
case .desktop:
return strings().loginNewCodeEnterCode
case .pirate:
return strings().loginNewCodeEnterSms
}
}
}
private let playerView:LottiePlayerView = LottiePlayerView()
private let header: TextView = TextView()
private let desc: TextView = TextView()
private(set) var type: IconType = .phone
private var descAttr: NSAttributedString?
required init(frame frameRect: NSRect) {
super.init(frame: frameRect)
addSubview(playerView)
addSubview(header)
addSubview(desc)
header.userInteractionEnabled = false
header.isSelectable = false
desc.isSelectable = false
updateLocalizationAndTheme(theme: theme)
}
override func updateLocalizationAndTheme(theme: PresentationTheme) {
super.updateLocalizationAndTheme(theme: theme)
let theme = theme as! TelegramPresentationTheme
let layout = TextViewLayout(.initialize(string: self.type.header, color: theme.colors.text, font: Auth_Insets.headerFont))
layout.measure(width: frame.width)
self.header.update(layout)
if let descAttr = descAttr {
let layout = TextViewLayout(descAttr, alignment: .center)
layout.interactions = globalLinkExecutor
layout.measure(width: frame.width)
self.desc.update(layout)
}
switch self.type {
case .pirate:
if let data = LocalAnimatedSticker.pirate_flag.data {
self.playerView.set(LottieAnimation(compressed: data, key: .init(key: .bundle("pirate_flag"), size: Auth_Insets.logoSize, backingScale: Int(System.backingScale), fitzModifier: nil), playPolicy: .loop, colors: []))
}
default:
if let data = LocalAnimatedSticker.code_note.data {
var colors:[LottieColor] = []
colors.append(.init(keyPath: "Bubble.Group 1.Fill 1", color: theme.colors.accent))
colors.append(.init(keyPath: "Note.Path.Fill 1", color: theme.colors.grayText))
colors.append(.init(keyPath: "Note.Path-2.Stroke 1", color: theme.colors.grayText))
colors.append(.init(keyPath: "Phone.Combined-Shape.Fill 1", color: theme.colors.grayText))
self.playerView.set(LottieAnimation(compressed: data, key: .init(key: .bundle("code_note"), size: Auth_Insets.logoSize, backingScale: Int(System.backingScale), fitzModifier: nil), playPolicy: .onceEnd, colors: colors))
}
}
self.layout()
}
func update(desc: NSAttributedString, type: IconType) {
self.descAttr = desc
self.type = type
updateLocalizationAndTheme(theme: theme)
}
override func layout() {
super.layout()
self.playerView.setFrameSize(Auth_Insets.logoSize)
self.playerView.centerX(y: 0)
self.header.centerX(y: self.playerView.frame.maxY + 20)
self.desc.centerX(y: self.header.frame.maxY + 10)
}
func playAnimation() {
playerView.playAgain()
}
var height: CGFloat {
return self.desc.frame.maxY
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
final class Auth_CodeEntryView: View {
private let header: Auth_CodeEntryHeaderView
private let container = View()
private let control = Auth_CodeEntryContol(frame: .zero)
private let error: LoginErrorStateView = LoginErrorStateView()
private let nextView: Auth_NextView = Auth_NextView()
private var takeResend:(()->Void)?
private var nextTextView: TextView?
private let disposable = MetaDisposable()
private var fragmentUrl: String?
required init(frame frameRect: NSRect) {
header = Auth_CodeEntryHeaderView(frame: frameRect.size.bounds)
super.init(frame: frameRect)
container.addSubview(header)
container.addSubview(control)
container.addSubview(error)
container.addSubview(nextView)
nextView.set(handler: { [weak self] _ in
if self?.header.type == .pirate {
self?.openFragment()
} else {
self?.control.invoke()
}
}, for: .Click)
addSubview(container)
updateLocalizationAndTheme(theme: theme)
}
override func updateLocalizationAndTheme(theme: PresentationTheme) {
super.updateLocalizationAndTheme(theme: theme)
nextView.updateLocalizationAndTheme(theme: theme)
}
override func layout() {
super.layout()
container.setFrameSize(NSMakeSize(frame.width, header.height + Auth_Insets.betweenHeader + control.frame.height + Auth_Insets.betweenNextView + Auth_Insets.nextHeight))
header.setFrameSize(NSMakeSize(frame.width, header.height))
header.centerX(y: 0)
control.centerX(y: header.frame.maxY + Auth_Insets.betweenHeader)
error.centerX(y: control.frame.maxY + Auth_Insets.betweenError)
nextView.centerX(y: control.frame.maxY + Auth_Insets.betweenNextView)
container.center()
nextTextView?.centerX(y: container.frame.maxY + Auth_Insets.betweenHeader)
}
func applyExternalLoginCode(_ code: String) {
if !code.isEmpty {
let chars = Array(code).map { String($0) }
self.control.insertAll(chars.compactMap { Int($0) })
}
}
func update(locked: Bool, error: AuthorizationCodeVerificationError?, number: String, type: SentAuthorizationCodeType, timeout: Int32?, nextType: AuthorizationCodeNextType?, takeEdit:@escaping()->Void, takeNext:@escaping(String)->Void, takeResend: @escaping()->Void, takeError:@escaping()->Void) {
let info: String
let iconType: Auth_CodeEntryHeaderView.IconType
let length: Int32
var nextString: String = strings().loginNext
var detectBold = true
switch type {
case let .otherSession(_length):
length = _length
info = strings().loginNewCodeCodeInfo(formatPhoneNumber(number))
iconType = .desktop
case let .sms(_length):
length = _length
info = strings().loginNewCodeSmsInfo(formatPhoneNumber(number))
iconType = .phone
case let .call(_length):
length = _length
iconType = .phone
info = strings().loginNewCodeCallInfo(formatPhoneNumber(number))
case let .fragment(url, _length):
length = _length
iconType = .pirate
info = strings().loginNewCodeFragmentInfo(formatPhoneNumber(number))
nextString = strings().loginNextFragment
self.fragmentUrl = url
case let .email(pattern, _length, _, _, _, _):
length = _length
info = strings().loginNewCodeEmail(pattern)
iconType = .phone
detectBold = false
default:
fatalError()
}
let attr = parseMarkdownIntoAttributedString(info, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: Auth_Insets.infoFont, textColor: theme.colors.grayText), bold: MarkdownAttributeSet(font: Auth_Insets.infoFontBold, textColor: theme.colors.grayText), link: MarkdownAttributeSet(font: Auth_Insets.infoFont, textColor: theme.colors.link), linkAttribute: { contents in
return (NSAttributedString.Key.link.rawValue, inAppLink.callback(contents, { url in
if url.isEmpty {
takeEdit()
} else {
execute(inapp: .external(link: url, false))
}
}))
})).mutableCopy() as! NSMutableAttributedString
if detectBold {
attr.detectBoldColorInString(with: .medium(.header))
}
self.header.update(desc: attr, type: iconType)
let size = self.control.update(count: Int(length))
self.control.setFrameSize(size)
self.control.takeNext = takeNext
self.control.takeError = takeError
self.control.set(locked: locked, animated: true)
if let error = error {
let textError:String
switch error {
case .limitExceeded:
textError = strings().loginFloodWait
case .invalidCode:
textError = strings().phoneCodeInvalid
case .generic:
textError = strings().phoneCodeExpired
case .codeExpired:
textError = strings().phoneCodeExpired
case .invalidEmailToken:
fatalError()
case .invalidEmailAddress:
fatalError()
}
self.error.state.set(.error(textError))
self.control.shake()
self.control.moveToStart()
} else {
self.error.state.set(.normal)
}
nextView.updateLocked(locked, string: nextString)
self.takeResend = takeResend
needsLayout = true
if let timeout = timeout {
runNextTimer(type, nextType, timeout)
}
updateAfterTick(type, nextType, timeout)
}
private func openFragment() {
if let url = self.fragmentUrl {
execute(inapp: .external(link: url, false))
}
}
deinit {
disposable.dispose()
}
func runNextTimer(_ type:SentAuthorizationCodeType, _ nextType:AuthorizationCodeNextType?, _ timeout:Int32) {
disposable.set(countdown(Double(timeout), delay: 1).start(next: { [weak self] value in
self?.updateAfterTick(type, nextType, Int32(value))
}, completed: {
}))
}
func updateAfterTick(_ type:SentAuthorizationCodeType, _ nextType:AuthorizationCodeNextType?, _ timeout:Int32?) {
var nextText:String = ""
switch type {
case .otherSession:
nextText = strings().loginSendSmsIfNotReceivedAppCode
default:
break
}
if let nextType = nextType {
if let timeout = timeout {
let timeout = Int(timeout)
let minutes = timeout / 60;
let sec = timeout % 60;
let secValue = sec > 9 ? "\(sec)" : "0\(sec)"
if timeout > 0 {
switch nextType {
case .call:
nextText = strings().loginWillCall(minutes, secValue)
break
case .sms:
nextText = strings().loginWillSendSms(minutes, secValue)
break
default:
break
}
} else {
switch nextType {
case .call:
nextText = strings().loginPhoneDialed
break
default:
break
}
}
} else {
nextText = strings().loginSendSmsIfNotReceivedAppCode
}
}
var nextLayout: TextViewLayout? = nil
if !nextText.isEmpty {
let attr = NSMutableAttributedString()
if case .otherSession = type {
_ = attr.append(string: nextText, color: theme.colors.link , font: .normal(.title))
attr.add(link: inAppLink.callback("resend", { [weak self] _ in
self?.takeResend?()
}), for: attr.range)
if timeout == nil {
attr.addAttribute(NSAttributedString.Key.foregroundColor, value: theme.colors.link, range: attr.range)
} else if let timeout = timeout {
attr.addAttribute(NSAttributedString.Key.foregroundColor, value: timeout <= 0 ? theme.colors.link : theme.colors.grayText, range: attr.range)
}
} else {
_ = attr.append(string: nextText, color: theme.colors.grayText, font: .normal(.title))
}
let layout = TextViewLayout(attr)
layout.interactions = globalLinkExecutor
layout.measure(width: frame.width - 40)
nextLayout = layout
}
if let nextLayout = nextLayout {
let current: TextView
if let nextTextView = nextTextView {
current = nextTextView
} else {
current = TextView()
self.nextTextView = current
addSubview(current)
}
var point = focus(nextLayout.layoutSize).origin
point.y = container.frame.maxY + 30
current.update(nextLayout, origin: point)
} else if let nextTextView = nextTextView {
performSubviewRemoval(nextTextView, animated: true)
self.nextTextView = nil
}
needsLayout = true
}
func playAnimation() {
header.playAnimation()
control.moveToStart()
}
func firstResponder() -> NSResponder? {
return control.firstResponder()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
final class Auth_CodeEntryController : GenericViewController<Auth_CodeEntryView> {
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
}
override func viewDidLoad() {
super.viewDidLoad()
readyOnce()
}
override func firstResponder() -> NSResponder? {
return genericView.firstResponder()
}
func applyExternalLoginCode(_ code: String) {
self.genericView.applyExternalLoginCode(code)
}
func update(locked: Bool, error: AuthorizationCodeVerificationError?, number: String, type: SentAuthorizationCodeType, timeout: Int32?, nextType: AuthorizationCodeNextType?, takeEdit:@escaping()->Void, takeNext:@escaping(String)->Void, takeResend:@escaping()->Void, takeError:@escaping()->Void) {
self.genericView.update(locked: locked, error: error, number: number, type: type, timeout: timeout, nextType: nextType, takeEdit: takeEdit, takeNext: takeNext, takeResend: takeResend, takeError: takeError)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if animated {
genericView.playAnimation()
}
}
}