mirror of
https://github.com/ichmagmaus111/ghostgram.git
synced 2026-11-03 14:17:19 +01:00
Ghostgram for iOS, rebuilt from scratch on Telegram iOS 13.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
402 lines
20 KiB
Swift
402 lines
20 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import AsyncDisplayKit
|
|
import Display
|
|
import TelegramCore
|
|
import TelegramPresentationData
|
|
import PasscodeInputFieldNode
|
|
import ComponentFlow
|
|
import WalletSendKeyboardComponent
|
|
|
|
enum PasscodeSetupInitialState {
|
|
case createPasscode
|
|
case changePassword(current: String, hasRecoveryEmail: Bool, hasSecureValues: Bool)
|
|
}
|
|
|
|
enum PasscodeSetupStateKind: Int32 {
|
|
case enterPasscode
|
|
case confirmPasscode
|
|
}
|
|
|
|
final class PasscodeSetupControllerNode: ASDisplayNode {
|
|
private var presentationData: PresentationData
|
|
private var mode: PasscodeSetupControllerMode
|
|
private let useCustomNumericKeyboard: Bool
|
|
private let keyboard = ComponentView<Empty>()
|
|
private var isCustomInputActive = true
|
|
private var previousDisplaysCustomKeyboard: Bool?
|
|
|
|
private var displaysCustomKeyboard: Bool {
|
|
guard self.useCustomNumericKeyboard else { return false }
|
|
switch self.mode {
|
|
case let .setup(_, type):
|
|
return type != .alphanumeric
|
|
case let .entry(challenge):
|
|
switch challenge.passcodeKind {
|
|
case .digits4, .digits6:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
private let wrapperNode: ASDisplayNode
|
|
|
|
private let titleNode: ASTextNode
|
|
private let subtitleNode: ASTextNode
|
|
private let inputFieldNode: PasscodeInputFieldNode
|
|
private let modeButtonNode: HighlightableButtonNode
|
|
|
|
var previousPasscode: String?
|
|
var currentPasscode: String {
|
|
return self.inputFieldNode.text
|
|
}
|
|
|
|
var selectPasscodeMode: ((HighlightableButtonNode) -> Void)?
|
|
var checkPasscode: ((String) -> Void)?
|
|
var complete: ((String, Bool) -> Void)?
|
|
var updateNextAction: ((Bool) -> Void)?
|
|
|
|
private let hapticFeedback = HapticFeedback()
|
|
private var authenticationInputEnabled = true
|
|
|
|
private var validLayout: (ContainerViewLayout, CGFloat)?
|
|
|
|
init(presentationData: PresentationData, mode: PasscodeSetupControllerMode, useCustomNumericKeyboard: Bool) {
|
|
self.presentationData = presentationData
|
|
self.mode = mode
|
|
self.useCustomNumericKeyboard = useCustomNumericKeyboard
|
|
|
|
self.wrapperNode = ASDisplayNode()
|
|
|
|
self.titleNode = ASTextNode()
|
|
self.titleNode.isUserInteractionEnabled = false
|
|
self.titleNode.displaysAsynchronously = false
|
|
|
|
self.subtitleNode = ASTextNode()
|
|
self.subtitleNode.isUserInteractionEnabled = false
|
|
self.subtitleNode.displaysAsynchronously = false
|
|
|
|
let passcodeType: PasscodeEntryFieldType
|
|
switch self.mode {
|
|
case let .entry(challenge):
|
|
switch challenge.passcodeKind {
|
|
case .digits4: passcodeType = .digits4
|
|
case .digits6: passcodeType = .digits6
|
|
default: passcodeType = .alphanumeric
|
|
}
|
|
case let .setup(_, type):
|
|
passcodeType = type
|
|
}
|
|
|
|
self.inputFieldNode = PasscodeInputFieldNode(color: self.presentationData.theme.list.itemPrimaryTextColor, accentColor: self.presentationData.theme.list.itemAccentColor, fieldType: passcodeType, keyboardAppearance: self.presentationData.theme.rootController.keyboardColor.keyboardAppearance, useCustomNumpad: self.useCustomNumericKeyboard && passcodeType != .alphanumeric, fieldBackgroundColor: self.presentationData.theme.list.itemBlocksBackgroundColor)
|
|
|
|
self.modeButtonNode = HighlightableButtonNode()
|
|
self.modeButtonNode.setTitle(self.presentationData.strings.PasscodeSettings_PasscodeOptions, with: Font.regular(17.0), with: self.presentationData.theme.list.itemAccentColor, for: .normal)
|
|
|
|
super.init()
|
|
|
|
self.setViewBlock({
|
|
return UITracingLayerView()
|
|
})
|
|
|
|
self.backgroundColor = self.presentationData.theme.list.blocksBackgroundColor
|
|
self.clipsToBounds = self.useCustomNumericKeyboard
|
|
|
|
self.addSubnode(self.wrapperNode)
|
|
|
|
self.wrapperNode.addSubnode(self.titleNode)
|
|
self.wrapperNode.addSubnode(self.subtitleNode)
|
|
self.wrapperNode.addSubnode(self.inputFieldNode)
|
|
self.wrapperNode.addSubnode(self.modeButtonNode)
|
|
|
|
let text: String
|
|
switch self.mode {
|
|
case .entry:
|
|
self.modeButtonNode.isHidden = true
|
|
self.modeButtonNode.isAccessibilityElement = false
|
|
text = self.presentationData.strings.EnterPasscode_EnterPasscode
|
|
case let .setup(change, _):
|
|
if change {
|
|
text = self.presentationData.strings.EnterPasscode_EnterNewPasscodeChange
|
|
} else {
|
|
text = self.presentationData.strings.EnterPasscode_EnterNewPasscodeNew
|
|
}
|
|
}
|
|
self.titleNode.attributedText = NSAttributedString(string: text, font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
|
|
|
|
self.inputFieldNode.complete = { [weak self] passcode in
|
|
guard let self, self.currentPasscode == passcode else { return }
|
|
self.activateNext()
|
|
}
|
|
|
|
self.modeButtonNode.addTarget(self, action: #selector(self.modePressed), forControlEvents: .touchUpInside)
|
|
}
|
|
|
|
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
|
self.validLayout = (layout, navigationBarHeight)
|
|
self.updateCustomKeyboard(layout: layout, transition: transition)
|
|
|
|
let standardInputHeight = layout.deviceMetrics.standardInputHeight(inLandscape: layout.orientation == .landscape)
|
|
var insets = layout.insets(options: [.statusBar])
|
|
if self.displaysCustomKeyboard {
|
|
insets.bottom = standardInputHeight + layout.additionalInsets.bottom
|
|
} else {
|
|
let keyboardHeight = max(layout.inputHeight ?? 0.0, standardInputHeight)
|
|
insets.bottom = max(insets.bottom, keyboardHeight)
|
|
}
|
|
|
|
self.wrapperNode.frame = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height)
|
|
|
|
let inputFieldFrame = self.inputFieldNode.updateLayout(size: layout.size, topOffset: floor(insets.top + navigationBarHeight + (layout.size.height - navigationBarHeight - insets.top - insets.bottom - 24.0) / 2.0), transition: transition)
|
|
transition.updateFrame(node: self.inputFieldNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
|
|
|
let titleSize = self.titleNode.measure(CGSize(width: layout.size.width - 28.0, height: CGFloat.greatestFiniteMagnitude))
|
|
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - titleSize.width) / 2.0), y: inputFieldFrame.minY - titleSize.height - 20.0), size: titleSize))
|
|
|
|
let subtitleSize = self.subtitleNode.measure(CGSize(width: layout.size.width - 28.0, height: CGFloat.greatestFiniteMagnitude))
|
|
transition.updateFrame(node: self.subtitleNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - subtitleSize.width) / 2.0), y: inputFieldFrame.maxY + 20.0), size: subtitleSize))
|
|
|
|
transition.updateFrame(node: self.modeButtonNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom - 53.0), size: CGSize(width: layout.size.width, height: 44.0)))
|
|
}
|
|
|
|
override func didLoad() {
|
|
super.didLoad()
|
|
self.view.disablesInteractiveKeyboardGestureRecognizer = true
|
|
}
|
|
|
|
private func updateCustomKeyboard(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
|
let isVisible = self.displaysCustomKeyboard
|
|
let visibilityChanged = self.previousDisplaysCustomKeyboard.map { $0 != isVisible } ?? false
|
|
self.previousDisplaysCustomKeyboard = isVisible
|
|
guard isVisible || self.keyboard.view != nil else { return }
|
|
|
|
var minimumKeyboardHeight = layout.deviceMetrics.standardInputHeight(inLandscape: layout.orientation == .landscape) - 49.0
|
|
if UIDevice.current.userInterfaceIdiom == .pad {
|
|
minimumKeyboardHeight = min(minimumKeyboardHeight, 270.0)
|
|
}
|
|
let isEnabled = isVisible && self.authenticationInputEnabled && self.isCustomInputActive
|
|
let keyboardSize = self.keyboard.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(WalletSendKeyboardComponent(
|
|
theme: self.presentationData.theme,
|
|
safeInsets: layout.safeInsets,
|
|
isLandscape: layout.size.width > layout.size.height && layout.metrics.widthClass == .compact,
|
|
minimumHeight: minimumKeyboardHeight,
|
|
mode: .numeric,
|
|
deleteTitle: self.presentationData.strings.Common_Delete,
|
|
isEnabled: isEnabled,
|
|
action: { [weak self] action in
|
|
guard let self, self.displaysCustomKeyboard, self.authenticationInputEnabled, self.isCustomInputActive, self.view.isUserInteractionEnabled else { return }
|
|
self.hapticFeedback.impact(.light)
|
|
switch action {
|
|
case let .insertText(text):
|
|
self.inputFieldNode.append(text)
|
|
case .deleteBackward:
|
|
let _ = self.inputFieldNode.delete()
|
|
}
|
|
}
|
|
)),
|
|
environment: {},
|
|
containerSize: layout.size
|
|
)
|
|
if let keyboardView = self.keyboard.view {
|
|
if keyboardView.superview == nil {
|
|
self.view.addSubview(keyboardView)
|
|
keyboardView.frame = CGRect(x: 0.0, y: layout.size.height, width: keyboardSize.width, height: keyboardSize.height)
|
|
}
|
|
if isVisible {
|
|
keyboardView.isHidden = false
|
|
}
|
|
keyboardView.isUserInteractionEnabled = isEnabled
|
|
keyboardView.accessibilityElementsHidden = !isEnabled
|
|
let keyboardFrame = CGRect(
|
|
x: 0.0,
|
|
y: isVisible ? layout.size.height - layout.additionalInsets.bottom - keyboardSize.height : layout.size.height,
|
|
width: keyboardSize.width,
|
|
height: keyboardSize.height
|
|
)
|
|
// Input activation and system keyboard updates can repeat the same target during the slide.
|
|
if keyboardView.frame != keyboardFrame {
|
|
var keyboardTransition = transition
|
|
if visibilityChanged || (!transition.isAnimated && keyboardView.layer.animation(forKey: "position") != nil) {
|
|
keyboardTransition = .animated(duration: 0.25, curve: .easeInOut)
|
|
}
|
|
keyboardTransition.updateFrame(view: keyboardView, frame: keyboardFrame, beginWithCurrentState: true, completion: { [weak self, weak keyboardView] completed in
|
|
guard completed, let self, let keyboardView,
|
|
!self.displaysCustomKeyboard, keyboardView.frame == keyboardFrame else { return }
|
|
keyboardView.isHidden = true
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func deactivateCustomInput() {
|
|
guard self.useCustomNumericKeyboard else { return }
|
|
self.isCustomInputActive = false
|
|
self.inputFieldNode.isInputEnabled = false
|
|
self.inputFieldNode.cancelPendingCompletion()
|
|
if let validLayout = self.validLayout {
|
|
self.updateCustomKeyboard(layout: validLayout.0, transition: .immediate)
|
|
}
|
|
}
|
|
|
|
func updateInputEnabled(_ enabled: Bool) {
|
|
self.authenticationInputEnabled = enabled
|
|
self.inputFieldNode.isInputEnabled = enabled && (!self.useCustomNumericKeyboard || self.isCustomInputActive)
|
|
self.modeButtonNode.isUserInteractionEnabled = enabled
|
|
if let validLayout = self.validLayout {
|
|
self.updateCustomKeyboard(layout: validLayout.0, transition: .immediate)
|
|
}
|
|
}
|
|
|
|
func updateMode(_ mode: PasscodeSetupControllerMode) {
|
|
(self.keyboard.view as? WalletSendKeyboardComponent.View)?.cancelKeyPresses()
|
|
self.mode = mode
|
|
self.inputFieldNode.reset()
|
|
|
|
if case let .setup(_, type) = mode {
|
|
self.inputFieldNode.updateFieldType(type, animated: true, useCustomNumpad: self.displaysCustomKeyboard)
|
|
|
|
if case .alphanumeric = type {
|
|
self.updateNextAction?(true)
|
|
} else {
|
|
self.updateNextAction?(false)
|
|
}
|
|
self.subtitleNode.isHidden = true
|
|
}
|
|
if let validLayout = self.validLayout {
|
|
self.containerLayoutUpdated(validLayout.0, navigationBarHeight: validLayout.1, transition: .animated(duration: 0.25, curve: .easeInOut))
|
|
}
|
|
}
|
|
|
|
func activateNext() {
|
|
guard self.authenticationInputEnabled else { return }
|
|
if self.useCustomNumericKeyboard && !self.isCustomInputActive {
|
|
return
|
|
}
|
|
if case let .setup(_, type) = self.mode, let maxLength = type.maxLength, self.currentPasscode.count != maxLength {
|
|
return
|
|
}
|
|
(self.keyboard.view as? WalletSendKeyboardComponent.View)?.cancelKeyPresses()
|
|
guard !self.currentPasscode.isEmpty else {
|
|
self.animateError()
|
|
return
|
|
}
|
|
|
|
switch self.mode {
|
|
case .entry:
|
|
self.checkPasscode?(self.currentPasscode)
|
|
case .setup:
|
|
if let previousPasscode = self.previousPasscode {
|
|
if self.currentPasscode == previousPasscode {
|
|
var numerical = false
|
|
if case let .setup(_, type) = mode {
|
|
if case .alphanumeric = type {
|
|
} else {
|
|
numerical = true
|
|
}
|
|
}
|
|
self.complete?(self.currentPasscode, numerical)
|
|
} else {
|
|
self.previousPasscode = nil
|
|
|
|
if let snapshotView = self.wrapperNode.view.snapshotContentTree() {
|
|
snapshotView.frame = self.wrapperNode.frame
|
|
self.wrapperNode.view.superview?.insertSubview(snapshotView, aboveSubview: self.wrapperNode.view)
|
|
snapshotView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: self.wrapperNode.bounds.width, y: 0.0), duration: 0.25, removeOnCompletion: false, additive: true, completion : { [weak snapshotView] _ in
|
|
snapshotView?.removeFromSuperview()
|
|
})
|
|
self.wrapperNode.layer.animatePosition(from: CGPoint(x: -self.wrapperNode.bounds.width, y: 0.0), to: CGPoint(), duration: 0.25, additive: true)
|
|
|
|
self.inputFieldNode.reset(animated: false)
|
|
self.titleNode.attributedText = NSAttributedString(string: self.presentationData.strings.EnterPasscode_EnterNewPasscodeChange, font: Font.regular(16.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
|
|
self.subtitleNode.isHidden = false
|
|
self.subtitleNode.attributedText = NSAttributedString(string: self.presentationData.strings.PasscodeSettings_DoNotMatch, font: Font.regular(16.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
|
|
self.modeButtonNode.isHidden = false
|
|
self.modeButtonNode.isAccessibilityElement = true
|
|
|
|
UIAccessibility.post(notification: UIAccessibility.Notification.announcement, argument: self.presentationData.strings.PasscodeSettings_DoNotMatch)
|
|
|
|
if let validLayout = self.validLayout {
|
|
self.containerLayoutUpdated(validLayout.0, navigationBarHeight: validLayout.1, transition: .immediate)
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
self.previousPasscode = self.currentPasscode
|
|
|
|
if let snapshotView = self.wrapperNode.view.snapshotContentTree() {
|
|
snapshotView.frame = self.wrapperNode.frame
|
|
self.wrapperNode.view.superview?.insertSubview(snapshotView, aboveSubview: self.wrapperNode.view)
|
|
snapshotView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: -self.wrapperNode.bounds.width, y: 0.0), duration: 0.25, removeOnCompletion: false, additive: true, completion : { [weak snapshotView] _ in
|
|
snapshotView?.removeFromSuperview()
|
|
})
|
|
self.wrapperNode.layer.animatePosition(from: CGPoint(x: self.wrapperNode.bounds.width, y: 0.0), to: CGPoint(), duration: 0.25, additive: true)
|
|
|
|
self.inputFieldNode.reset(animated: false)
|
|
self.titleNode.attributedText = NSAttributedString(string: self.presentationData.strings.EnterPasscode_RepeatNewPasscode, font: Font.regular(16.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
|
|
self.subtitleNode.isHidden = true
|
|
self.modeButtonNode.isHidden = true
|
|
self.modeButtonNode.isAccessibilityElement = false
|
|
|
|
UIAccessibility.post(notification: UIAccessibility.Notification.announcement, argument: self.presentationData.strings.EnterPasscode_RepeatNewPasscode)
|
|
|
|
if let validLayout = self.validLayout {
|
|
self.containerLayoutUpdated(validLayout.0, navigationBarHeight: validLayout.1, transition: .immediate)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func activateInput() {
|
|
guard self.authenticationInputEnabled else { return }
|
|
self.isCustomInputActive = true
|
|
self.inputFieldNode.isInputEnabled = true
|
|
self.inputFieldNode.activateInput()
|
|
if let validLayout = self.validLayout {
|
|
self.updateCustomKeyboard(layout: validLayout.0, transition: .immediate)
|
|
}
|
|
|
|
UIAccessibility.post(notification: UIAccessibility.Notification.announcement, argument: self.titleNode.attributedText?.string)
|
|
}
|
|
|
|
func animateError() {
|
|
self.inputFieldNode.reset()
|
|
self.inputFieldNode.layer.addShakeAnimation(amplitude: -30.0, duration: 0.5, count: 6, decay: true)
|
|
|
|
self.hapticFeedback.error()
|
|
}
|
|
|
|
func updateAuthenticationState(_ state: SettingsPasscodeAuthentication.State) {
|
|
self.updateInputEnabled(state == .ready)
|
|
self.inputFieldNode.isUserInteractionEnabled = self.authenticationInputEnabled
|
|
let message: String?
|
|
switch state {
|
|
case .ready: message = ""
|
|
case .cooldown:
|
|
message = self.presentationData.strings.PasscodeSettings_TryAgainIn1Minute
|
|
self.inputFieldNode.reset(animated: false)
|
|
case .checking: message = nil
|
|
case .finished:
|
|
message = ""
|
|
self.inputFieldNode.reset(animated: false)
|
|
self.view.endEditing(true)
|
|
}
|
|
if let message {
|
|
self.subtitleNode.attributedText = NSAttributedString(string: message, font: Font.regular(16.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
|
|
self.subtitleNode.isHidden = message.isEmpty
|
|
if let validLayout = self.validLayout {
|
|
self.containerLayoutUpdated(validLayout.0, navigationBarHeight: validLayout.1, transition: .immediate)
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc func modePressed() {
|
|
self.deactivateCustomInput()
|
|
self.selectPasscodeMode?(self.modeButtonNode)
|
|
}
|
|
}
|