Files
ghostgram/submodules/QrCodeUI/Sources/QrCodeScanScreen.swift
T
ichmagmaus111andClaude Opus 5.5 534cabe35b bug fixes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 16:55:08 +01:00

1070 lines
47 KiB
Swift

import Foundation
import UIKit
import AccountContext
import AsyncDisplayKit
import Display
import ComponentFlow
import SwiftSignalKit
import Camera
import CameraLegacy
import CoreImage
import AlertUI
import TelegramPresentationData
import TelegramCore
import UndoUI
import Markdown
import TextFormat
import LegacyUI
import LegacyComponents
import LegacyMediaPickerUI
import ImageContentAnalysis
import PresentationDataUtils
import BundleIconComponent
import GlassBarButtonComponent
private func parseAuthTransferUrl(_ url: URL) -> Data? {
var tokenString: String?
if let query = url.query, let components = URLComponents(string: "/?" + query), let queryItems = components.queryItems {
for queryItem in queryItems {
if let value = queryItem.value {
if queryItem.name == "token", !value.isEmpty {
tokenString = value
}
}
}
}
if var tokenString = tokenString {
tokenString = tokenString.replacingOccurrences(of: "-", with: "+")
tokenString = tokenString.replacingOccurrences(of: "_", with: "/")
while tokenString.count % 4 != 0 {
tokenString.append("=")
}
if let data = Data(base64Encoded: tokenString) {
return data
}
}
return nil
}
private func normalizedTonQrValue(_ value: String) -> String? {
let value = value.trimmingCharacters(in: .whitespacesAndNewlines)
guard !value.isEmpty else {
return nil
}
if let components = URLComponents(string: value),
components.scheme?.lowercased() == "ton",
components.host?.lowercased() == "transfer" {
return value
}
let friendlyCharacters = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-_"))
if value.count == 48, value.unicodeScalars.allSatisfy({ friendlyCharacters.contains($0) }) {
return value
}
let rawParts = value.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: false)
let hexadecimalCharacters = CharacterSet(charactersIn: "0123456789abcdefABCDEF")
if rawParts.count == 2,
(rawParts[0] == "0" || rawParts[0] == "-1"),
rawParts[1].count == 64,
rawParts[1].unicodeScalars.allSatisfy({ hexadecimalCharacters.contains($0) }) {
return value
}
return nil
}
public final class QrCodeScanScreen: ViewController {
public enum Subject {
case authTransfer(activeSessionsContext: ActiveSessionsContext)
case peer
case cryptoAddress
case custom(info: String)
case customValidated(info: String, validate: (String) -> Bool)
}
public static func normalizedCryptoAddress(_ value: String) -> String? {
return normalizedTonQrValue(value)
}
private let context: AccountContext
private let subject: QrCodeScanScreen.Subject
private var presentationData: PresentationData
private var codeDisposable: Disposable?
private var inForegroundDisposable: Disposable?
private let approveDisposable = MetaDisposable()
private var controllerNode: QrCodeScanScreenNode {
return self.displayNode as! QrCodeScanScreenNode
}
public var showMyCode: () -> Void = {}
public var completion: (String?) -> Void = { _ in }
public var dismissed: (() -> Void)?
private var codeResolved = false
private var validLayout: ContainerViewLayout?
public init(context: AccountContext, subject: QrCodeScanScreen.Subject) {
self.context = context
self.subject = subject
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
super.init(navigationBarPresentationData: nil)
self.statusBar.statusBarStyle = .White
self.navigationPresentation = .modalInLargeLayout
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
self.navigationBar?.intrinsicCanTransitionInline = false
self.inForegroundDisposable = (context.sharedContext.applicationBindings.applicationInForeground
|> deliverOnMainQueue).start(next: { [weak self] inForeground in
guard let strongSelf = self else {
return
}
(strongSelf.displayNode as! QrCodeScanScreenNode).updateInForeground(inForeground)
})
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.codeDisposable?.dispose()
self.inForegroundDisposable?.dispose()
self.approveDisposable.dispose()
}
@objc fileprivate func cancelPressed() {
self.completion(nil)
self.dismissAnimated()
}
private var animatedIn = false
public override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let isCustom: Bool
switch self.subject {
case .custom:
isCustom = true
default:
isCustom = false
}
if isCustom, !self.animatedIn, let layout = self.validLayout {
self.animatedIn = true
self.controllerNode.layer.animatePosition(from: CGPoint(x: 0.0, y: layout.size.height), to: CGPoint(), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
}
}
private func dismissWithSession(session: RecentAccountSession?) {
guard case let .authTransfer(activeSessionsContext) = self.subject else {
return
}
if let navigationController = navigationController as? NavigationController {
self.present(UndoOverlayController(presentationData: self.presentationData, content: .actionSucceeded(title: self.presentationData.strings.AuthSessions_AddedDeviceTitle, text: session?.appName ?? "Telegram for macOS", cancel: self.presentationData.strings.AuthSessions_AddedDeviceTerminate, destructive: true), elevatedLayout: false, animateInAsReplacement: false, action: { value in
if value == .undo, let session = session {
let _ = activeSessionsContext.remove(hash: session.hash).start()
return true
} else {
return false
}
}), in: .window(.root))
var viewControllers = navigationController.viewControllers
viewControllers = viewControllers.filter { controller in
if controller is RecentSessionsController {
return false
}
if controller === self {
return false
}
return true
}
viewControllers.append(self.context.sharedContext.makeRecentSessionsController(context: self.context, activeSessionsContext: activeSessionsContext))
navigationController.setViewControllers(viewControllers, animated: true)
} else {
self.dismiss()
}
}
public func dismissAnimated() {
guard let layout = self.validLayout else {
return
}
self.controllerNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: layout.size.height), duration: 0.2, removeOnCompletion: false, additive: true, completion: { _ in
self.dismiss()
})
}
@discardableResult
fileprivate func completeWithCode(_ code: String) -> Bool {
switch self.subject {
case .cryptoAddress:
guard let value = normalizedTonQrValue(code) else {
return false
}
self.codeResolved = true
self.completion(value)
self.dismissAnimated()
return true
case .custom:
self.completion(code)
return true
case let .customValidated(_, validate):
guard validate(code) else {
return false
}
self.codeResolved = true
self.completion(code)
return true
default:
return false
}
}
override public func loadDisplayNode() {
self.displayNode = QrCodeScanScreenNode(context: self.context, presentationData: self.presentationData, controller: self, subject: self.subject)
self.displayNodeDidLoad()
self.codeDisposable = ((self.displayNode as! QrCodeScanScreenNode).focusedCode.get()
|> map { code -> String? in
return code?.message
}
|> distinctUntilChanged
|> mapToSignal { code -> Signal<String?, NoError> in
return .single(code)
|> delay(0.5, queue: Queue.mainQueue())
}).start(next: { [weak self] code in
guard let strongSelf = self, !strongSelf.codeResolved else {
return
}
guard let code = code else {
return
}
switch strongSelf.subject {
case let .authTransfer(activeSessionsContext):
if let url = URL(string: code), let parsedToken = parseAuthTransferUrl(url) {
strongSelf.approveDisposable.set((approveAuthTransferToken(account: strongSelf.context.account, token: parsedToken, activeSessionsContext: activeSessionsContext)
|> deliverOnMainQueue).start(next: { session in
guard let strongSelf = self else {
return
}
strongSelf.controllerNode.codeWithError = nil
if case let .authTransfer(activeSessionsContext) = strongSelf.subject {
Queue.mainQueue().after(1.5, {
activeSessionsContext.loadMore()
})
}
strongSelf.dismissWithSession(session: session)
}, error: { _ in
guard let strongSelf = self else {
return
}
strongSelf.controllerNode.codeWithError = code
strongSelf.controllerNode.updateFocusedRect(nil)
}))
}
case .cryptoAddress:
strongSelf.completeWithCode(code)
case .peer:
if let _ = URL(string: code) {
strongSelf.controllerNode.resolveCode(code: code, completion: { [weak self] result in
if let strongSelf = self {
strongSelf.codeResolved = true
}
})
}
case .custom, .customValidated:
strongSelf.completeWithCode(code)
}
})
self.controllerNode.present = { [weak self] c in
self?.present(c, in: .window(.root))
}
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.validLayout = layout
(self.displayNode as! QrCodeScanScreenNode).containerLayoutUpdated(layout: layout, navigationHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
}
}
private final class FrameNode: ASDisplayNode {
let topLeftLine: CAShapeLayer
let topRightLine: CAShapeLayer
let bottomLeftLine: CAShapeLayer
let bottomRightLine: CAShapeLayer
override init() {
self.topLeftLine = CAShapeLayer()
self.topRightLine = CAShapeLayer()
self.bottomLeftLine = CAShapeLayer()
self.bottomRightLine = CAShapeLayer()
super.init()
for line in self.lines {
line.strokeColor = UIColor.white.cgColor
line.fillColor = UIColor.clear.cgColor
line.lineWidth = 4.0
line.lineCap = .round
self.layer.addSublayer(line)
}
}
private var lines: [CAShapeLayer] {
return [
self.topLeftLine,
self.topRightLine,
self.bottomLeftLine,
self.bottomRightLine
]
}
func animateIn() {
let strokeStart = self.topLeftLine.strokeStart
let strokeEnd = self.topLeftLine.strokeEnd
let duration: Double = 0.85
let delay: Double = 0.15
for line in self.lines {
line.animateSpring(from: 0.0 as NSNumber, to: strokeStart as NSNumber, keyPath: "strokeStart", duration: duration, delay: delay)
line.animateSpring(from: 1.0 as NSNumber, to: strokeEnd as NSNumber, keyPath: "strokeEnd", duration: duration, delay: delay)
}
}
func updateLayout(size: CGSize) {
let cornerRadius: CGFloat = 6.0
let lineLength = size.width / 2.0 - cornerRadius
let targetLineLength = 24.0
let fraction = targetLineLength / lineLength
let strokeFraction = (1.0 - fraction) / 2.0
let strokeStart = strokeFraction
let strokeEnd = 1.0 - strokeFraction
let topLeftPath = CGMutablePath()
topLeftPath.move(to: CGPoint(x: 0.0, y: size.height / 2.0))
topLeftPath.addArc(center: CGPoint(x: cornerRadius, y: cornerRadius), radius: cornerRadius, startAngle: -.pi, endAngle: -.pi / 2.0, clockwise: false)
topLeftPath.addLine(to: CGPoint(x: size.width / 2.0, y: 0.0))
self.topLeftLine.path = topLeftPath
self.topLeftLine.strokeStart = strokeStart
self.topLeftLine.strokeEnd = strokeEnd
let topRightPath = CGMutablePath()
topRightPath.move(to: CGPoint(x: size.width / 2.0, y: 0.0))
topRightPath.addArc(center: CGPoint(x: size.width - cornerRadius, y: cornerRadius), radius: cornerRadius, startAngle: -.pi / 2.0, endAngle: 0.0, clockwise: false)
topRightPath.addLine(to: CGPoint(x: size.width, y: size.height / 2.0))
self.topRightLine.path = topRightPath
self.topRightLine.strokeStart = strokeStart
self.topRightLine.strokeEnd = strokeEnd
let bottomRightPath = CGMutablePath()
bottomRightPath.move(to: CGPoint(x: size.width, y: size.height / 2.0))
bottomRightPath.addArc(center: CGPoint(x: size.width - cornerRadius, y: size.height - cornerRadius), radius: cornerRadius, startAngle: 0.0, endAngle: .pi / 2.0, clockwise: false)
bottomRightPath.addLine(to: CGPoint(x: size.width / 2.0, y: size.height))
self.bottomRightLine.path = bottomRightPath
self.bottomRightLine.strokeStart = strokeStart
self.bottomRightLine.strokeEnd = strokeEnd
let bottomLeftPath = CGMutablePath()
bottomLeftPath.move(to: CGPoint(x: size.width / 2.0, y: size.height))
bottomLeftPath.addArc(center: CGPoint(x: cornerRadius, y: size.height - cornerRadius), radius: cornerRadius, startAngle: .pi / 2.0, endAngle: .pi, clockwise: false)
bottomLeftPath.addLine(to: CGPoint(x: 0.0, y: size.height / 2.0))
self.bottomLeftLine.path = bottomLeftPath
self.bottomLeftLine.strokeStart = strokeStart
self.bottomLeftLine.strokeEnd = strokeEnd
for line in self.lines {
line.frame = CGRect(origin: .zero, size: size)
}
}
}
private final class QrCodeScanScreenNode: ViewControllerTracingNode, ASScrollViewDelegate {
private let context: AccountContext
private var presentationData: PresentationData
private weak var controller: QrCodeScanScreen?
private let subject: QrCodeScanScreen.Subject
private let previewView: CameraSimplePreviewView
private let fadeNode: ASDisplayNode
private let dimLayer: SimpleShapeLayer
private let frameNode: FrameNode
private let galleryButtonNode: GlassButtonNode
private let torchButtonNode: GlassButtonNode
private let titleNode: ImmediateTextNode
private let textNode: ImmediateTextNode
private let errorTextNode: ImmediateTextNode
private let topNavigationButton = ComponentView<Empty>()
private let camera: CameraProtocol
private let codeDisposable = MetaDisposable()
private var torchDisposable: Disposable?
private let resolveDisposable = MetaDisposable()
fileprivate let focusedCode = ValuePromise<CameraCode?>(ignoreRepeated: true)
private var focusedRect: CGRect?
var present: (ViewController) -> Void = { _ in }
private var validLayout: (ContainerViewLayout, CGFloat)?
var codeWithError: String? {
didSet {
if self.codeWithError != oldValue {
if self.codeWithError != nil {
self.errorTextNode.isHidden = false
} else {
self.errorTextNode.isHidden = true
}
}
}
}
private var highlightViews: [UIVisualEffectView] = []
init(context: AccountContext, presentationData: PresentationData, controller: QrCodeScanScreen, subject: QrCodeScanScreen.Subject) {
self.context = context
self.presentationData = presentationData
self.controller = controller
self.subject = subject
let cameraImpl = LegacyCameraImpl.shared
self.previewView = cameraImpl.makeCameraSimplePreviewView(frame: .zero, main: true, roundVideo: false)
self.previewView.backgroundColor = .black
self.fadeNode = ASDisplayNode()
self.fadeNode.alpha = 0.0
self.fadeNode.backgroundColor = .black
self.dimLayer = SimpleShapeLayer()
self.dimLayer.fillRule = .evenOdd
self.dimLayer.fillColor = UIColor(rgb: 0x000000, alpha: 0.8).cgColor
self.dimLayer.opacity = 0.625
self.frameNode = FrameNode()
self.galleryButtonNode = GlassButtonNode(icon: UIImage(bundleImageName: "Wallet/CameraGalleryIcon")!, label: nil)
self.torchButtonNode = GlassButtonNode(icon: UIImage(bundleImageName: "Wallet/CameraFlashIcon")!, label: nil)
let title: String
var text: String
switch subject {
case .authTransfer:
title = presentationData.strings.AuthSessions_AddDevice_ScanTitle
text = presentationData.strings.AuthSessions_AddDevice_ScanInstallInfo
case .peer:
title = ""
text = ""
case .cryptoAddress:
title = ""
text = ""
case let .custom(info):
title = presentationData.strings.AuthSessions_AddDevice_ScanTitle
text = info
case let .customValidated(info, _):
title = presentationData.strings.AuthSessions_AddDevice_ScanTitle
text = info
}
self.titleNode = ImmediateTextNode()
self.titleNode.displaysAsynchronously = false
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.bold(32.0), textColor: .white)
self.titleNode.maximumNumberOfLines = 0
self.titleNode.textAlignment = .center
let textFont = Font.regular(17.0)
let boldFont = Font.bold(17.0)
text = text.replacingOccurrences(of: " [", with: " [").replacingOccurrences(of: ") ", with: ") ")
let attributedText = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: .white), bold: MarkdownAttributeSet(font: boldFont, textColor: .white), link: MarkdownAttributeSet(font: boldFont, textColor: .white), linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
})))
self.textNode = ImmediateTextNode()
self.textNode.displaysAsynchronously = false
self.textNode.attributedText = attributedText
self.textNode.maximumNumberOfLines = 0
self.textNode.textAlignment = .center
self.textNode.lineSpacing = 0.5
self.errorTextNode = ImmediateTextNode()
self.errorTextNode.displaysAsynchronously = false
self.errorTextNode.attributedText = NSAttributedString(string: presentationData.strings.AuthSessions_AddDevice_InvalidQRCode, font: Font.medium(16.0), textColor: .white)
self.errorTextNode.maximumNumberOfLines = 0
self.errorTextNode.textAlignment = .center
self.errorTextNode.isHidden = true
self.camera = cameraImpl.makeCamera(
configuration: .init(preset: .hd1920x1080, position: .back, audio: false, photo: true, metadata: true),
previewView: self.previewView,
secondaryPreviewView: nil
)
super.init()
self.backgroundColor = .black
self.torchDisposable = (self.camera.hasTorch
|> deliverOnMainQueue).start(next: { [weak self] hasTorch in
if let strongSelf = self {
strongSelf.torchButtonNode.isHidden = !hasTorch
}
})
self.addSubnode(self.fadeNode)
self.addSubnode(self.frameNode)
if case .peer = subject {
self.addSubnode(self.galleryButtonNode)
}
self.addSubnode(self.torchButtonNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
self.addSubnode(self.errorTextNode)
self.galleryButtonNode.pressed = { [weak self] in
self?.galleryPressed()
}
self.torchButtonNode.pressed = { [weak self] in
self?.torchPressed()
}
self.previewView.resetPlaceholder(front: false)
if #available(iOS 13.0, *) {
let _ = (self.previewView.isPreviewing
|> filter { $0 }
|> take(1)
|> deliverOnMainQueue).startStandalone(next: { [weak self] _ in
self?.previewView.removePlaceholder(delay: 0.15)
})
} else {
Queue.mainQueue().after(0.35) {
self.previewView.removePlaceholder(delay: 0.15)
}
}
}
deinit {
self.codeDisposable.dispose()
self.torchDisposable?.dispose()
self.resolveDisposable.dispose()
self.camera.stopCapture(invalidate: true)
}
fileprivate func updateInForeground(_ inForeground: Bool) {
if !inForeground {
self.camera.stopCapture(invalidate: false)
} else {
self.camera.startCapture()
}
}
override func didLoad() {
super.didLoad()
self.view.insertSubview(self.previewView, at: 0)
self.layer.insertSublayer(self.dimLayer, above: self.fadeNode.layer)
self.camera.startCapture()
var detectedCodes = self.camera.detectedCodes
#if DEBUG && targetEnvironment(simulator)
if let code = UIPasteboard.general.string?.trimmingCharacters(in: .whitespacesAndNewlines), !code.isEmpty {
detectedCodes = .single([CameraCode(type: .qr, message: code, corners: [
CGPoint(x: 0.3, y: 0.3),
CGPoint(x: 0.7, y: 0.3),
CGPoint(x: 0.7, y: 0.7),
CGPoint(x: 0.3, y: 0.7)
])])
}
#endif
let throttledSignal = detectedCodes
|> mapToThrottled { next -> Signal<[CameraCode], NoError> in
return .single(next) |> then(.complete() |> delay(0.3, queue: Queue.concurrentDefaultQueue()))
}
self.codeDisposable.set((throttledSignal
|> deliverOnMainQueue).start(next: { [weak self] codes in
guard let strongSelf = self else {
return
}
let filteredCodes: [CameraCode]
switch strongSelf.subject {
case .authTransfer:
filteredCodes = codes.filter { $0.message.hasPrefix("tg://") }
case .peer:
filteredCodes = codes.filter { $0.message.hasPrefix("https://t.me/") || $0.message.hasPrefix("t.me/") }
case .cryptoAddress:
filteredCodes = codes.filter { normalizedTonQrValue($0.message) != nil }
case .custom:
filteredCodes = codes
case let .customValidated(_, validate):
filteredCodes = codes.filter { validate($0.message) }
}
if let code = filteredCodes.first, CGRect(x: 0.3, y: 0.3, width: 0.4, height: 0.4).contains(code.boundingBox.center) {
if strongSelf.codeWithError != code.message {
strongSelf.codeWithError = nil
}
if strongSelf.codeWithError == code.message {
strongSelf.focusedCode.set(nil)
strongSelf.updateFocusedRect(nil)
} else {
strongSelf.focusedCode.set(code)
strongSelf.updateFocusedRect(code.boundingBox)
}
} else {
strongSelf.codeWithError = nil
strongSelf.focusedCode.set(nil)
strongSelf.updateFocusedRect(nil)
}
}))
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
recognizer.tapActionAtPoint = { _ in
return .waitForSingleTap
}
self.textNode.view.addGestureRecognizer(recognizer)
}
private var animatedIn = false
func animateIn() {
guard !self.animatedIn else {
return
}
self.animatedIn = true
self.frameNode.animateIn()
}
@objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
switch recognizer.state {
case .ended:
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
switch gesture {
case .tap:
if let (_, attributes) = self.textNode.attributesAtPoint(location) {
if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
switch url {
case "desktop":
self.context.sharedContext.openExternalUrl(context: self.context, urlContext: .generic, url: "https://getdesktop.telegram.org", forceExternal: true, presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: {})
case "web":
self.context.sharedContext.openExternalUrl(context: self.context, urlContext: .generic, url: "https://web.telegram.org", forceExternal: true, presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: {})
default:
break
}
}
}
default:
break
}
}
default:
break
}
}
func updateFocusedRect(_ rect: CGRect?) {
self.focusedRect = rect
if let (layout, navigationHeight) = self.validLayout {
self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.4, curve: .spring))
}
}
private func updateDimPath(bounds: CGRect, cutoutRect: CGRect, transition: ContainedViewLayoutTransition, delay: Double, completion: @escaping () -> Void) {
let path = CGMutablePath()
path.addRect(bounds)
path.addRect(cutoutRect)
guard self.dimLayer.path != path else {
completion()
return
}
let previousPath: CGPath?
if self.dimLayer.animation(forKey: "path") != nil {
previousPath = self.dimLayer.presentation()?.path ?? self.dimLayer.path
} else {
previousPath = self.dimLayer.path
}
self.dimLayer.path = path
if case let .animated(duration, curve) = transition, let previousPath {
self.dimLayer.animate(from: previousPath, to: path, keyPath: "path", timingFunction: curve.timingFunction, duration: duration, delay: delay, mediaTimingFunction: curve.mediaTimingFunction, completion: { _ in
completion()
})
} else {
self.dimLayer.removeAnimation(forKey: "path")
completion()
}
}
private var animatingIn = false
func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, animateIn: Bool = false, transition: ContainedViewLayoutTransition) {
let hadLayout = self.validLayout != nil
self.validLayout = (layout, navigationHeight)
var prepareForAnimateIn = false
if !hadLayout {
prepareForAnimateIn = true
}
let sideInset: CGFloat = 66.0
let titleSpacing: CGFloat = 48.0
let bounds = CGRect(origin: CGPoint(), size: layout.size)
// The camera preview follows the device into landscape where this screen can be landscape: an iPad, and iPhone
// Duo's unfolded screen (regular width, so this portrait-only-on-phones screen rotates there too).
var previewFollowsDeviceOrientation = false
if case .tablet = layout.deviceMetrics.type {
previewFollowsDeviceOrientation = true
} else if case .iPhoneDuo = layout.deviceMetrics, case .regular = layout.metrics.widthClass {
previewFollowsDeviceOrientation = true
}
if previewFollowsDeviceOrientation {
if UIDevice.current.orientation == .landscapeLeft {
self.previewView.layer.transform = CATransform3DMakeRotation(-CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
} else if UIDevice.current.orientation == .landscapeRight {
self.previewView.layer.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
} else {
self.previewView.layer.transform = CATransform3DIdentity
}
}
transition.updateFrame(view: self.previewView, frame: bounds)
transition.updateFrame(node: self.fadeNode, frame: bounds)
let topNavigationIconName: String
let isCustom: Bool
switch self.subject {
case .custom:
isCustom = true
default:
isCustom = false
}
if isCustom {
topNavigationIconName = "Navigation/Close"
} else {
topNavigationIconName = "Navigation/Back"
}
let topNavigationButtonSide = CGSize(width: 44.0, height: 44.0)
let topNavigationButtonSize = self.topNavigationButton.update(
transition: ComponentTransition(transition),
component: AnyComponent(GlassBarButtonComponent(
size: topNavigationButtonSide,
backgroundColor: nil,
isDark: true,
state: .glass,
component: AnyComponentWithIdentity(id: topNavigationIconName, component: AnyComponent(
BundleIconComponent(
name: topNavigationIconName,
tintColor: .white
)
)),
action: { [weak self] _ in
guard let self else {
return
}
let isCustom: Bool
switch self.subject {
case .custom:
isCustom = true
default:
isCustom = false
}
if isCustom {
self.controller?.cancelPressed()
} else {
self.controller?.dismiss()
}
}
)),
environment: {},
containerSize: topNavigationButtonSide
)
let topNavigationButtonTopInset: CGFloat = (layout.statusBarHeight ?? 0.0) == 0.0 ? 16.0 : 10.0
if let topNavigationButtonView = self.topNavigationButton.view {
if topNavigationButtonView.superview == nil {
self.view.addSubview(topNavigationButtonView)
}
let topNavigationButtonFrame: CGRect
if let sideColumnFrame = layout.sideColumnNavigationButtonFrame {
// iPhone Duo: the back / close button goes in the side strip, as every screen's does.
topNavigationButtonFrame = CGRect(origin: CGPoint(x: sideColumnFrame.midX - topNavigationButtonSize.width / 2.0, y: sideColumnFrame.midY - topNavigationButtonSize.height / 2.0), size: topNavigationButtonSize)
} else if layout.topBarStatusIsland != nil {
// iPhone Duo, unfolded portrait: on the status island's clock line, as the camera's and editors' top rows
// (the island is at the other end of the row).
topNavigationButtonFrame = CGRect(
origin: CGPoint(
x: 16.0 + layout.safeInsets.left,
y: (layout.navigationBarStatusBarHeight ?? 0.0) + 31.0 - topNavigationButtonSize.height / 2.0
),
size: topNavigationButtonSize
)
} else {
topNavigationButtonFrame = CGRect(
origin: CGPoint(
x: 16.0 + layout.safeInsets.left,
y: max(layout.statusBarHeight ?? 0.0, layout.safeInsets.top) + topNavigationButtonTopInset
),
size: topNavigationButtonSize
)
}
transition.updateFrame(view: topNavigationButtonView, frame: topNavigationButtonFrame)
}
// The viewfinder, its buttons and texts center on the screen, or in the band between the side insets when
// they differ (iPhone Duo's side strip). Equal insets keep every frame unchanged.
let insetsDiffer = layout.safeInsets.left != layout.safeInsets.right
let bandMinX: CGFloat = insetsDiffer ? layout.safeInsets.left : 0.0
let bandWidth: CGFloat = insetsDiffer ? layout.size.width - layout.safeInsets.left - layout.safeInsets.right : layout.size.width
// On a landscape-shaped screen (iPhone Duo's unfolded screen, 951x669) it also fits the height, with room for the
// texts above it and the torch below.
var frameSide = max(240.0, bandWidth - sideInset * 2.0)
if layout.size.width > layout.size.height {
frameSide = max(240.0, min(frameSide, layout.size.height - 240.0))
}
let animateInScale: CGFloat = 0.4
var effectiveFrameSide = frameSide
if prepareForAnimateIn {
effectiveFrameSide = round(effectiveFrameSide * animateInScale)
}
let dimHeight = ceil((layout.size.height - frameSide) / 2.0)
let effectiveDimHeight = ceil((layout.size.height - effectiveFrameSide) / 2.0)
let dimInset = bandMinX + (bandWidth - frameSide) / 2.0
let effectiveDimInset = bandMinX + (bandWidth - effectiveFrameSide) / 2.0
let dimAlpha: CGFloat
let dimRect: CGRect
let frameRect: CGRect
let controlsAlpha: CGFloat
let frameAlpha: CGFloat = 1.0
if let focusedRect = self.focusedRect {
controlsAlpha = 0.0
dimAlpha = 1.0
let side = max(bounds.width * focusedRect.width, bounds.height * focusedRect.height) * 0.6
let center = CGPoint(x: (1.0 - focusedRect.center.y) * bounds.width, y: focusedRect.center.x * bounds.height)
dimRect = CGRect(x: center.x - side / 2.0, y: center.y - side / 2.0, width: side, height: side)
frameRect = dimRect
} else {
controlsAlpha = 1.0
dimAlpha = 0.625
dimRect = CGRect(x: effectiveDimInset, y: effectiveDimHeight, width: effectiveFrameSide, height: layout.size.height - effectiveDimHeight * 2.0)
frameRect = CGRect(x: dimInset, y: dimHeight, width: frameSide, height: layout.size.height - dimHeight * 2.0)
}
transition.updateAlpha(layer: self.dimLayer, alpha: dimAlpha)
transition.updateAlpha(node: self.frameNode, alpha: frameAlpha)
if !self.animatingIn {
var delay: Double = 0.0
if animateIn {
self.animatingIn = true
delay = 0.1
}
let dimTransition: ContainedViewLayoutTransition = self.dimLayer.path == nil ? .immediate : transition
dimTransition.updateFrame(layer: self.dimLayer, frame: bounds, delay: delay)
self.updateDimPath(bounds: bounds, cutoutRect: dimRect, transition: dimTransition, delay: delay, completion: { [weak self] in
self?.animatingIn = false
})
transition.updateFrame(node: self.frameNode, frame: frameRect, beginWithCurrentState: true)
self.frameNode.updateLayout(size: frameRect.size)
if animateIn {
transition.animateTransformScale(node: self.frameNode, from: CGPoint(x: animateInScale, y: animateInScale), delay: delay)
}
}
let buttonSize = CGSize(width: 72.0, height: 72.0)
var torchFrame = CGRect(origin: CGPoint(x: bandMinX + floor((bandWidth - buttonSize.width) / 2.0), y: dimHeight + frameSide + 98.0), size: buttonSize)
let updatedTorchY = min(torchFrame.minY, layout.size.height - torchFrame.height - 10.0)
let additionalTorchOffset: CGFloat = updatedTorchY - torchFrame.minY
torchFrame.origin.y = updatedTorchY
var galleryFrame = torchFrame
if case .peer = self.subject {
galleryFrame.origin.x -= buttonSize.width
torchFrame.origin.x += buttonSize.width
}
transition.updateFrame(node: self.galleryButtonNode, frame: galleryFrame)
transition.updateFrame(node: self.torchButtonNode, frame: torchFrame)
transition.updateAlpha(node: self.textNode, alpha: controlsAlpha)
transition.updateAlpha(node: self.errorTextNode, alpha: controlsAlpha)
transition.updateAlpha(node: self.galleryButtonNode, alpha: controlsAlpha)
transition.updateAlpha(node: self.torchButtonNode, alpha: controlsAlpha)
for view in self.highlightViews {
transition.updateAlpha(layer: view.layer, alpha: controlsAlpha)
}
let titleSize = self.titleNode.updateLayout(CGSize(width: bandWidth - 16.0, height: layout.size.height))
let textSize = self.textNode.updateLayout(CGSize(width: bandWidth - 16.0, height: layout.size.height))
let errorTextSize = self.errorTextNode.updateLayout(CGSize(width: bandWidth - 16.0, height: layout.size.height))
var textFrame = CGRect(origin: CGPoint(x: bandMinX + floor((bandWidth - textSize.width) / 2.0), y: max(dimHeight - textSize.height - titleSpacing, navigationHeight + floorToScreenPixels((dimHeight - navigationHeight - textSize.height) / 2.0) + 5.0)), size: textSize)
let titleFrame = CGRect(origin: CGPoint(x: bandMinX + floor((bandWidth - titleSize.width) / 2.0), y: textFrame.minY - 18.0 - titleSize.height), size: titleSize)
if titleFrame.minY < navigationHeight {
transition.updateAlpha(node: self.titleNode, alpha: 0.0)
textFrame = textFrame.offsetBy(dx: 0.0, dy: -5.0)
} else {
transition.updateAlpha(node: self.titleNode, alpha: controlsAlpha)
}
var errorTextFrame = CGRect(origin: CGPoint(x: bandMinX + floor((bandWidth - errorTextSize.width) / 2.0), y: dimHeight + frameSide + 48.0), size: errorTextSize)
errorTextFrame.origin.y += floor(additionalTorchOffset / 2.0)
transition.updateFrameAdditive(node: self.titleNode, frame: titleFrame)
transition.updateFrameAdditive(node: self.textNode, frame: textFrame)
transition.updateFrameAdditive(node: self.errorTextNode, frame: errorTextFrame)
if self.highlightViews.isEmpty {
let urlAttributesAndRects = self.textNode.cachedLayout?.allAttributeRects(name: "UrlAttributeT") ?? []
for (_, rect) in urlAttributesAndRects {
let view = UIVisualEffectView(effect: UIBlurEffect(style: .light))
view.clipsToBounds = true
view.layer.cornerRadius = 5.0
view.frame = rect.offsetBy(dx: self.textNode.frame.minX, dy: self.textNode.frame.minY).insetBy(dx: -4.0, dy: -2.0)
self.view.insertSubview(view, belowSubview: self.textNode.view)
self.highlightViews.append(view)
}
}
if prepareForAnimateIn {
self.animateIn()
self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, animateIn: true, transition: .animated(duration: 0.8, curve: .customSpring(damping: 88.0, initialVelocity: 0.0)))
}
}
@objc private func galleryPressed() {
let context = self.context
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let presentError = { [weak self] in
let alertController = textAlertController(context: context, title: nil, text: presentationData.strings.Contacts_QrCode_NoCodeFound, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])
self?.present(alertController)
}
let _ = legacyWallpaperPicker(context: context, presentationData: presentationData, subject: .qrCode).start(next: { [weak self] generator in
let legacyController = LegacyController(presentation: .modal(animateIn: true), theme: presentationData.theme)
legacyController.statusBar.statusBarStyle = presentationData.theme.rootController.statusBarStyle.style
let controller = generator(legacyController.context)
legacyController.bind(controller: controller)
legacyController.deferScreenEdgeGestures = [.top]
controller.selectionBlock = { [weak legacyController] asset, _ in
if let asset = asset {
TGMediaAssetImageSignals.image(for: asset, imageType: TGMediaAssetImageTypeScreen, size: CGSize(width: 1280.0, height: 1280.0)).start(next: { image in
if let image = image as? UIImage {
let _ = (recognizeQRCode(in: image)
|> deliverOnMainQueue).start(next: { [weak self] result in
if let result = result, let strongSelf = self {
strongSelf.resolveCode(code: result, completion: { result in
if result {
} else {
presentError()
}
})
} else {
presentError()
}
})
} else {
presentError()
}
}, error: { _ in
presentError()
}, completed: {
})
legacyController?.dismiss()
}
}
controller.dismissalBlock = { [weak legacyController] in
if let legacyController = legacyController {
legacyController.dismiss()
}
}
self?.present(legacyController)
})
}
@objc private func torchPressed() {
self.torchButtonNode.isSelected = !self.torchButtonNode.isSelected
self.camera.setTorchActive(self.torchButtonNode.isSelected)
}
fileprivate func resolveCode(code: String, completion: @escaping (Bool) -> Void) {
switch self.subject {
case .cryptoAddress, .customValidated:
completion(self.controller?.completeWithCode(code) == true)
return
default:
break
}
self.resolveDisposable.set((self.context.sharedContext.resolveUrl(context: self.context, peerId: nil, url: code, skipUrlAuth: false)
|> deliverOnMainQueue).start(next: { [weak self] result in
if let strongSelf = self {
completion(strongSelf.openResolved(result))
}
}))
}
private func openResolved(_ result: ResolvedUrl) -> Bool {
switch result {
case .peer, .stickerPack, .join, .wallpaper, .theme, .noPaidMessagesToken:
break
default:
return false
}
guard let navigationController = self.controller?.navigationController as? NavigationController else {
return false
}
self.context.sharedContext.openResolvedUrl(result, context: self.context, urlContext: .generic, navigationController: navigationController, forceExternal: false, forceUpdate: false, openPeer: { [weak self, navigationController] peer, navigation in
guard let strongSelf = self else {
return
}
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), subject: nil, keepStack: .always, peekData: nil, completion: { [weak navigationController] _ in
if let navigationController = navigationController {
var viewControllers = navigationController.viewControllers
viewControllers = viewControllers.filter { controller in
if controller is QrCodeScanScreen {
return false
}
if controller is ChatQrCodeScreen {
return false
}
return true
}
navigationController.setViewControllers(viewControllers, animated: false)
}
}))
},
sendFile: nil,
sendSticker: nil,
sendEmoji: nil,
requestMessageActionUrlAuth: nil,
joinVoiceChat: { peerId, invite, call in
}, present: { [weak self] c, a in
self?.controller?.present(c, in: .window(.root), with: a)
}, dismissInput: { [weak self] in
self?.view.endEditing(true)
}, contentContext: nil, progress: nil, completion: nil)
return true
}
}