Files
TelegramSwift/Telegram-Mac/PasscodeLockController.swift
Mikhail Filimonov 248623baf1 - bugfixes
2024-07-02 11:20:09 -03:00

550 lines
19 KiB
Swift

//
// PasscodeLockController.swift
// TelegramMac
//
// Created by keepcoder on 10/01/2017.
// Copyright © 2017 Telegram. All rights reserved.
//
import Cocoa
import TGUIKit
import TelegramCore
import Postbox
import SwiftSignalKit
import LocalAuthentication
import BuildConfig
private class TouchIdContainerView : View {
fileprivate let button: TextButton = TextButton()
required init(frame frameRect: NSRect) {
super.init(frame: frameRect)
addSubview(button)
button.scaleOnClick = true
button.autohighlight = false
button.style = ControlStyle(font: .medium(.title), foregroundColor: theme.colors.underSelectedColor, backgroundColor: theme.colors.accent, highlightColor: theme.colors.accent)
button.set(font: .medium(.title), for: .Normal)
button.set(color: theme.colors.underSelectedColor, for: .Normal)
button.set(text: strings().passcodeUseTouchId, for: .Normal)
button.set(image: theme.icons.passcodeTouchId, for: .Normal)
button.layer?.cornerRadius = 18
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layout() {
super.layout()
_ = button.sizeToFit(NSMakeSize(16, 0), NSMakeSize(0, 36), thatFit: true)
button.centerX(y: frame.height - button.frame.height)
}
override func draw(_ layer: CALayer, in ctx: CGContext) {
super.draw(layer, in: ctx)
let (text, layout) = TextNode.layoutText(NSAttributedString.initialize(string: strings().passcodeOr, color: theme.chatServiceItemTextColor, font: .normal(.title)), theme.colors.background, 1, .end, NSMakeSize(.greatestFiniteMagnitude, .greatestFiniteMagnitude), nil, false, .center)
let f = focus(text.size)
layout.draw(NSMakeRect(f.minX, 0, f.width, f.height), in: ctx, backingScaleFactor: backingScaleFactor, backgroundColor: backgroundColor)
ctx.setFillColor(theme.chatServiceItemTextColor.cgColor)
ctx.fill(NSMakeRect(0, floorToScreenPixels(backingScaleFactor, f.height / 2), f.minX - 10, .borderSize))
ctx.setFillColor(theme.chatServiceItemTextColor.cgColor)
ctx.fill(NSMakeRect(f.maxX + 10, floorToScreenPixels(backingScaleFactor, f.height / 2), f.minX - 10, .borderSize))
}
}
final class PasscodeField : NSSecureTextField {
override func resignFirstResponder() -> Bool {
(self.delegate as? PasscodeLockView)?.controlTextDidBeginEditing(Notification(name: NSControl.textDidChangeNotification))
return super.resignFirstResponder()
}
override func becomeFirstResponder() -> Bool {
(self.delegate as? PasscodeLockView)?.controlTextDidEndEditing(Notification(name: NSControl.textDidChangeNotification))
return super.becomeFirstResponder()
}
}
class PasscodeLockView : Control, NSTextFieldDelegate {
private let backgroundView: BackgroundView = BackgroundView(frame: .zero)
private let visualEffect: NSVisualEffectView = NSVisualEffectView(frame: .zero)
fileprivate let nameView:TextView = TextView()
let input:PasscodeField
private let nextButton:ImageButton = ImageButton()
private var hasTouchId:Bool = false
private let touchIdContainer:TouchIdContainerView = TouchIdContainerView(frame: NSMakeRect(0, 0, 240, 76))
fileprivate let logoutTextView:TextView = TextView()
let value:ValuePromise<String> = ValuePromise(ignoreRepeated: false)
var logoutImpl:() -> Void = {}
fileprivate var useTouchIdImpl:() -> Void = {}
private let inputContainer: View = View()
private var fieldState: SearchFieldState = .Focus
required init(frame frameRect: NSRect) {
input = PasscodeField(frame: NSZeroRect)
input.stringValue = ""
backgroundView.useSharedAnimationPhase = false
super.init(frame: frameRect)
addSubview(backgroundView)
// addSubview(visualEffect)
visualEffect.state = .active
visualEffect.blendingMode = .withinWindow
autoresizingMask = [.width, .height]
self.backgroundColor = .clear
nextButton.autohighlight = false
nextButton.set(background: theme.colors.accentIcon, for: .Normal)
nextButton.set(image: theme.icons.passcodeLogin, for: .Normal)
nextButton.setFrameSize(26, 26)
nextButton.scaleOnClick = true
nextButton.layer?.cornerRadius = nextButton.frame.height / 2
nameView.userInteractionEnabled = false
nameView.isSelectable = false
addSubview(nextButton)
// addSubview(nameView)
addSubview(inputContainer)
addSubview(logoutTextView)
addSubview(touchIdContainer)
input.isBordered = false
input.usesSingleLineMode = true
input.isBezeled = false
input.focusRingType = .none
input.delegate = self
input.drawsBackground = false
nameView.disableBackgroundDrawing = true
logoutTextView.disableBackgroundDrawing = true
inputContainer.backgroundColor = theme.colors.grayBackground
inputContainer.layer?.cornerRadius = .cornerRadius
inputContainer.addSubview(input)
let attr = NSMutableAttributedString()
_ = attr.append(string: strings().passcodeEnterPasscodePlaceholder, color: theme.colors.grayText, font: .medium(17))
//attr.setAlignment(.center, range: attr.range)
input.placeholderAttributedString = attr
input.cell?.usesSingleLineMode = true
input.cell?.wraps = false
input.cell?.isScrollable = true
input.font = .normal(.title)
input.textView?.insertionPointColor = theme.colors.grayText
input.sizeToFit()
input.target = self
input.action = #selector(checkPasscode)
nextButton.set(handler: { [weak self] _ in
self?.checkPasscode()
}, for: .SingleClick)
touchIdContainer.button.set(handler: { [weak self] _ in
self?.useTouchIdImpl()
}, for: .SingleClick)
updateLocalizationAndTheme(theme: theme)
layout()
}
var containerBgColor: NSColor {
switch theme.controllerBackgroundMode {
case .gradient:
return theme.chatServiceItemColor
case .background:
return theme.chatServiceItemColor
default:
if theme.chatBackground == theme.chatServiceItemColor {
return theme.colors.grayBackground
}
}
return theme.chatServiceItemColor
}
var secondaryColor: NSColor {
switch theme.controllerBackgroundMode {
case .gradient:
return theme.chatServiceItemTextColor
case .background:
return theme.chatServiceItemTextColor
default:
if theme.chatBackground == theme.chatServiceItemColor {
return theme.colors.text
}
}
return theme.chatServiceItemTextColor
}
override func updateLocalizationAndTheme(theme: PresentationTheme) {
super.updateLocalizationAndTheme(theme: theme)
let theme = theme as! TelegramPresentationTheme
backgroundColor = theme.colors.background
input.backgroundColor = .clear
input.textView?.insertionPointColor = secondaryColor;
input.textColor = secondaryColor
inputContainer.background = containerBgColor
backgroundView.backgroundMode = theme.controllerBackgroundMode
let placeholder = NSMutableAttributedString()
_ = placeholder.append(string: strings().passcodeEnterPasscodePlaceholder, color: theme.chatServiceItemTextColor, font: .normal(.title))
input.placeholderAttributedString = placeholder
if #available(macOS 10.14, *) {
visualEffect.material = .underWindowBackground
} else {
visualEffect.material = theme.colors.isDark ? .ultraDark : .light
}
let logoutAttr = parseMarkdownIntoAttributedString(strings().passcodeLostDescription, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: .normal(.text), textColor: theme.chatServiceItemTextColor), bold: MarkdownAttributeSet(font: .bold(.text), textColor: theme.chatServiceItemTextColor), link: MarkdownAttributeSet(font: .bold(.text), textColor: secondaryColor == theme.chatServiceItemTextColor ? theme.chatServiceItemTextColor : theme.colors.link), linkAttribute: { contents in
return (NSAttributedString.Key.link.rawValue, inAppLink.callback(contents, {_ in}))
}))
logoutTextView.isSelectable = false
let logoutLayout = TextViewLayout(logoutAttr, alignment: .center)
logoutLayout.interactions = TextViewInteractions(processURL:{ [weak self] _ in
self?.logoutImpl()
})
logoutLayout.measure(width: frame.width - 40)
if theme.bubbled {
logoutLayout.generateAutoBlock(backgroundColor: theme.chatServiceItemColor)
}
logoutTextView.set(layout: logoutLayout)
let layout = TextViewLayout(.initialize(string: strings().passlockEnterYourPasscode, color: theme.chatServiceItemTextColor, font: .medium(17)), alignment: .center)
layout.measure(width: frame.width - 40)
if theme.bubbled {
layout.generateAutoBlock(backgroundColor: theme.chatServiceItemColor)
}
nameView.update(layout)
}
override func mouseMoved(with event: NSEvent) {
}
func controlTextDidChange(_ obj: Notification) {
change(state: fieldState, animated: true)
backgroundView.doAction()
}
func controlTextDidBeginEditing(_ obj: Notification) {
change(state: .Focus, animated: true)
input.textView?.insertionPointColor = secondaryColor
}
func controlTextDidEndEditing(_ obj: Notification) {
window?.makeFirstResponder(input)
input.textView?.insertionPointColor = secondaryColor
}
private func change(state: SearchFieldState, animated: Bool) {
self.fieldState = state
switch state {
case .Focus:
input._change(size: NSMakeSize(inputContainer.frame.width - 20, input.frame.height), animated: animated)
input._change(pos: NSMakePoint(10, input.frame.minY), animated: animated)
nextButton.change(opacity: 1, animated: animated)
nextButton._change(pos: NSMakePoint(inputContainer.frame.maxX + 10, nextButton.frame.minY), animated: animated)
case .None:
input.sizeToFit()
let f = inputContainer.focus(input.frame.size)
input._change(pos: NSMakePoint(f.minX, input.frame.minY), animated: animated)
nextButton.change(opacity: 0, animated: animated)
nextButton._change(pos: NSMakePoint(inputContainer.frame.maxX - nextButton.frame.width, nextButton.frame.minY), animated: animated)
}
}
@objc func checkPasscode() {
value.set(input.stringValue)
}
func update(hasTouchId: Bool) {
self.hasTouchId = hasTouchId
if hasTouchId {
showTouchIdUI()
} else {
hideTouchIdUI()
}
input.stringValue = ""
needsLayout = true
}
func updateAndSetValue() {
self.value.set(self.input.stringValue)
self.backgroundView.doAction()
}
private func showTouchIdUI() {
touchIdContainer.isHidden = false
}
private func hideTouchIdUI() {
touchIdContainer.isHidden = true
}
override func layout() {
super.layout()
backgroundView.frame = bounds
backgroundView.updateLayout(size: frame.size, transition: .immediate)
visualEffect.frame = bounds
inputContainer.setFrameSize(200, 36)
input.setFrameSize(inputContainer.frame.width - 20, input.frame.height)
input.center()
inputContainer.layer?.cornerRadius = inputContainer.frame.height / 2
logoutTextView.resize(frame.width - 40, blockColor: theme.chatServiceItemColor)
nameView.resize(frame.width - 40, blockColor: theme.chatServiceItemColor)
inputContainer.center()
inputContainer.setFrameOrigin(NSMakePoint(inputContainer.frame.minX - (nextButton.frame.width + 10) / 2, inputContainer.frame.minY))
touchIdContainer.centerX(y: inputContainer.frame.maxY + 10)
nextButton.setFrameOrigin(inputContainer.frame.maxX + 10, inputContainer.frame.minY + (inputContainer.frame.height - nextButton.frame.height) / 2)
nameView.centerX(y: inputContainer.frame.minY - nameView.frame.height - 10)
logoutTextView.centerX(y: frame.height - logoutTextView.frame.height - 10)
change(state: fieldState, animated: false)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class PasscodeLockController: ModalViewController {
let accountManager: AccountManager<TelegramAccountManagerTypes>
private let useTouchId: Bool
private let appearanceDisposable = MetaDisposable()
private let disposable:MetaDisposable = MetaDisposable()
private let valueDisposable = MetaDisposable()
private let logoutDisposable = MetaDisposable()
private let _doneValue:Promise<Bool> = Promise(false)
private let laContext = LAContext()
var doneValue:Signal<Bool, NoError> {
return _doneValue.get()
}
override func updateLocalizationAndTheme(theme: PresentationTheme) {
super.updateLocalizationAndTheme(theme: theme)
}
private let updateCurrectController: ()->Void
private let logoutImpl:() -> Signal<Never, NoError>
init(_ accountManager: AccountManager<TelegramAccountManagerTypes>, useTouchId: Bool, logoutImpl:@escaping()->Signal<Never, NoError> = { .complete() }, updateCurrectController: @escaping()->Void) {
self.accountManager = accountManager
self.logoutImpl = logoutImpl
self.useTouchId = useTouchId
self.updateCurrectController = updateCurrectController
super.init(frame: NSMakeRect(0, 0, 350, 350))
self.bar = .init(height: 0)
}
override var isVisualEffectBackground: Bool {
return false
}
override var isFullScreen: Bool {
return true
}
override var background: NSColor {
return self.containerBackground
}
private var genericView:PasscodeLockView {
return self.view as! PasscodeLockView
}
private func checkNextValue(_ passcode: String) {
let appEncryption = AppEncryptionParameters(path: accountManager.basePath.nsstring.deletingLastPathComponent)
appEncryption.applyPasscode(passcode)
if appEncryption.decrypt() != nil {
self._doneValue.set(.single(true))
self.close()
} else {
genericView.input.shake()
}
}
private var touchIdCalled = false
override func windowDidBecomeKey() {
super.windowDidBecomeKey()
if NSApp.isActive, useTouchId, !touchIdCalled {
callTouchId()
touchIdCalled = true
}
}
override func windowDidResignKey() {
super.windowDidResignKey()
if !NSApp.isActive, useTouchId {
}
}
func callTouchId() {
if laContext.canUseBiometric {
laContext.evaluatePolicy(.applicationPolicy, localizedReason: strings().passcodeUnlockTouchIdReason) { (success, evaluateError) in
if (success) {
Queue.mainQueue().async {
self._doneValue.set(.single(true))
self.close()
}
}
}
}
}
override var cornerRadius: CGFloat {
return 0
}
func invalidateTouchId() {
laContext.invalidate()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
self.updateCurrectController()
}
override func viewDidLoad() {
super.viewDidLoad()
appearanceDisposable.set(appearanceSignal.start(next: { [weak self] appearance in
self?.updateLocalizationAndTheme(theme: appearance.presentation)
}))
genericView.logoutImpl = { [weak self] in
guard let window = self?.window else { return }
verifyAlert_button(for: window, information: strings().accountConfirmLogoutText, successHandler: { [weak self] _ in
guard let `self` = self else { return }
_ = showModalProgress(signal: self.logoutImpl(), for: window).start(completed: { [weak self] in
delay(0.2, closure: { [weak self] in
self?.close()
})
})
})
}
genericView.useTouchIdImpl = { [weak self] in
self?.callTouchId()
}
valueDisposable.set((genericView.value.get() |> deliverOnMainQueue).start(next: { [weak self] value in
self?.checkNextValue(value)
}))
genericView.update(hasTouchId: useTouchId)
readyOnce()
}
override var closable: Bool {
return false
}
override func escapeKeyAction() -> KeyHandlerResult {
return .invoked
}
override func returnKeyAction() -> KeyHandlerResult {
self.genericView.updateAndSetValue()
return .invoked
}
override func firstResponder() -> NSResponder? {
if !(window?.firstResponder is NSText) {
return genericView.input
}
let editor = self.window?.fieldEditor(true, for: genericView.input)
if window?.firstResponder != editor {
return genericView.input
}
return editor
}
override var containerBackground: NSColor {
return theme.colors.background.withAlphaComponent(1.0)
}
override var handleEvents: Bool {
return true
}
override var handleAllEvents: Bool {
return true
}
override var responderPriority: HandlerPriority {
return .supreme
}
override var shouldCloseAllTheSameModals: Bool {
return false
}
deinit {
disposable.dispose()
logoutDisposable.dispose()
valueDisposable.dispose()
appearanceDisposable.dispose()
self.window?.removeAllHandlers(for: self)
}
override func viewClass() -> AnyClass {
return PasscodeLockView.self
}
}