mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 21:45:19 +00:00
382 lines
18 KiB
Swift
382 lines
18 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import SwiftSignalKit
|
|
import AppBundle
|
|
import AsyncDisplayKit
|
|
import Display
|
|
import QrCode
|
|
import AnimatedStickerNode
|
|
import SolidRoundedButtonNode
|
|
|
|
private func shareInvoiceQrCode(context: WalletContext, invoice: String) {
|
|
let _ = (qrCode(string: invoice, color: .black, backgroundColor: .white, icon: .custom(UIImage(bundleImageName: "Wallet/QrGem")))
|
|
|> map { _, generator -> UIImage? in
|
|
let imageSize = CGSize(width: 768.0, height: 768.0)
|
|
let context = generator(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), scale: 1.0))
|
|
return context?.generateImage()
|
|
}
|
|
|> deliverOnMainQueue).start(next: { image in
|
|
guard let image = image else {
|
|
return
|
|
}
|
|
|
|
let activityController = UIActivityViewController(activityItems: [image], applicationActivities: nil)
|
|
context.presentNativeController(activityController)
|
|
})
|
|
}
|
|
|
|
public enum WalletReceiveScreenMode {
|
|
case receive(address: String)
|
|
case invoice(address: String, amount: String?, comment: String?)
|
|
|
|
var address: String {
|
|
switch self {
|
|
case let .receive(address), let .invoice(address, _, _):
|
|
return address
|
|
}
|
|
}
|
|
}
|
|
|
|
final class WalletReceiveScreen: ViewController {
|
|
private let context: WalletContext
|
|
private let mode: WalletReceiveScreenMode
|
|
private var presentationData: WalletPresentationData
|
|
|
|
private let idleTimerExtensionDisposable: Disposable
|
|
|
|
public init(context: WalletContext, mode: WalletReceiveScreenMode) {
|
|
self.context = context
|
|
self.mode = mode
|
|
|
|
self.presentationData = context.presentationData
|
|
|
|
let defaultTheme = self.presentationData.theme.navigationBar
|
|
let navigationBarTheme = NavigationBarTheme(buttonColor: defaultTheme.buttonColor, disabledButtonColor: defaultTheme.disabledButtonColor, primaryTextColor: defaultTheme.primaryTextColor, backgroundColor: .clear, separatorColor: .clear, badgeBackgroundColor: defaultTheme.badgeBackgroundColor, badgeStrokeColor: defaultTheme.badgeStrokeColor, badgeTextColor: defaultTheme.badgeTextColor)
|
|
|
|
self.idleTimerExtensionDisposable = context.idleTimerExtension()
|
|
|
|
super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: navigationBarTheme, strings: NavigationBarStrings(back: self.presentationData.strings.Wallet_Navigation_Back, close: self.presentationData.strings.Wallet_Navigation_Close)))
|
|
|
|
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
|
|
self.navigationBar?.intrinsicCanTransitionInline = false
|
|
|
|
if case .receive = mode {
|
|
self.navigationPresentation = .flatModal
|
|
self.navigationItem.leftBarButtonItem = UIBarButtonItem(customView: UIView())
|
|
}
|
|
|
|
self.title = self.presentationData.strings.Wallet_Receive_Title
|
|
|
|
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Wallet_Navigation_Back, style: .plain, target: nil, action: nil)
|
|
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Wallet_Navigation_Done, style: .done, target: self, action: #selector(self.donePressed))
|
|
}
|
|
|
|
deinit {
|
|
self.idleTimerExtensionDisposable.dispose()
|
|
}
|
|
|
|
required init(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override public func loadDisplayNode() {
|
|
self.displayNode = WalletReceiveScreenNode(context: self.context, presentationData: self.presentationData, mode: self.mode)
|
|
(self.displayNode as! WalletReceiveScreenNode).openCreateInvoice = { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
self?.push(walletCreateInvoiceScreen(context: strongSelf.context, address: strongSelf.mode.address))
|
|
}
|
|
(self.displayNode as! WalletReceiveScreenNode).displayCopyContextMenu = { [weak self] node, frame, text in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text(title: strongSelf.presentationData.strings.Wallet_ContextMenuCopy, accessibilityLabel: strongSelf.presentationData.strings.Wallet_ContextMenuCopy), action: {
|
|
UIPasteboard.general.string = text
|
|
})])
|
|
strongSelf.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in
|
|
if let strongSelf = self {
|
|
return (node, frame.insetBy(dx: 0.0, dy: -2.0), strongSelf.displayNode, strongSelf.displayNode.view.bounds)
|
|
} else {
|
|
return nil
|
|
}
|
|
}))
|
|
}
|
|
self.displayNodeDidLoad()
|
|
}
|
|
|
|
override func preferredContentSizeForLayout(_ layout: ContainerViewLayout) -> CGSize? {
|
|
return CGSize(width: layout.size.width, height: min(674.0, layout.size.height))
|
|
}
|
|
|
|
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
|
super.containerLayoutUpdated(layout, transition: transition)
|
|
|
|
(self.displayNode as! WalletReceiveScreenNode).containerLayoutUpdated(layout: layout, navigationHeight: self.navigationHeight, transition: transition)
|
|
}
|
|
|
|
@objc private func donePressed() {
|
|
if let navigationController = self.navigationController as? NavigationController {
|
|
var controllers = navigationController.viewControllers
|
|
controllers = controllers.filter { controller in
|
|
if controller is WalletReceiveScreen {
|
|
return false
|
|
}
|
|
if controller is WalletCreateInvoiceScreen {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
navigationController.setViewControllers(controllers, animated: true)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func urlForMode(_ mode: WalletReceiveScreenMode) -> String {
|
|
switch mode {
|
|
case let .receive(address):
|
|
return walletInvoiceUrl(address: address)
|
|
case let .invoice(address, amount, comment):
|
|
return walletInvoiceUrl(address: address, amount: amount, comment: comment)
|
|
}
|
|
}
|
|
|
|
private final class WalletReceiveScreenNode: ViewControllerTracingNode {
|
|
private let context: WalletContext
|
|
private var presentationData: WalletPresentationData
|
|
private let mode: WalletReceiveScreenMode
|
|
|
|
private let textNode: ImmediateTextNode
|
|
|
|
private let qrButtonNode: HighlightTrackingButtonNode
|
|
private let qrImageNode: TransformImageNode
|
|
private let qrIconNode: AnimatedStickerNode
|
|
private var qrCodeSize: Int?
|
|
|
|
private let urlTextNode: ImmediateTextNode
|
|
|
|
private let buttonNode: SolidRoundedButtonNode
|
|
private let secondaryButtonNode: HighlightableButtonNode
|
|
|
|
private var validLayout: (ContainerViewLayout, CGFloat)?
|
|
|
|
var openCreateInvoice: (() -> Void)?
|
|
var displayCopyContextMenu: ((ASDisplayNode, CGRect, String) -> Void)?
|
|
|
|
init(context: WalletContext, presentationData: WalletPresentationData, mode: WalletReceiveScreenMode) {
|
|
self.context = context
|
|
self.presentationData = presentationData
|
|
self.mode = mode
|
|
|
|
self.textNode = ImmediateTextNode()
|
|
self.textNode.textAlignment = .center
|
|
self.textNode.maximumNumberOfLines = 5
|
|
|
|
self.qrImageNode = TransformImageNode()
|
|
self.qrImageNode.clipsToBounds = true
|
|
self.qrImageNode.cornerRadius = 14.0
|
|
|
|
self.qrIconNode = AnimatedStickerNode()
|
|
if let path = getAppBundle().path(forResource: "WalletIntroStatic", ofType: "tgs") {
|
|
self.qrIconNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 240, height: 240, mode: .direct)
|
|
self.qrIconNode.visibility = true
|
|
}
|
|
|
|
self.qrButtonNode = HighlightTrackingButtonNode()
|
|
|
|
self.urlTextNode = ImmediateTextNode()
|
|
self.urlTextNode.maximumNumberOfLines = 4
|
|
self.urlTextNode.textAlignment = .justified
|
|
self.urlTextNode.lineSpacing = 0.35
|
|
|
|
self.buttonNode = SolidRoundedButtonNode(title: "", icon: nil, theme: SolidRoundedButtonTheme(backgroundColor: self.presentationData.theme.setup.buttonFillColor, foregroundColor: self.presentationData.theme.setup.buttonForegroundColor), height: 50.0, cornerRadius: 10.0, gloss: false)
|
|
|
|
self.secondaryButtonNode = HighlightableButtonNode()
|
|
|
|
super.init()
|
|
|
|
self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
|
|
|
|
self.addSubnode(self.textNode)
|
|
self.addSubnode(self.qrImageNode)
|
|
self.addSubnode(self.qrIconNode)
|
|
self.addSubnode(self.qrButtonNode)
|
|
self.addSubnode(self.urlTextNode)
|
|
self.addSubnode(self.buttonNode)
|
|
self.addSubnode(self.secondaryButtonNode)
|
|
|
|
self.qrImageNode.setSignal(qrCode(string: urlForMode(mode), color: .black, backgroundColor: .white, icon: .cutout) |> beforeNext { [weak self] size, _ in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.qrCodeSize = size
|
|
if let (layout, navigationHeight) = strongSelf.validLayout {
|
|
strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate)
|
|
}
|
|
} |> map { $0.1 }, attemptSynchronously: true)
|
|
|
|
self.qrButtonNode.addTarget(self, action: #selector(self.qrPressed), forControlEvents: .touchUpInside)
|
|
self.qrButtonNode.highligthedChanged = { [weak self] highlighted in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if highlighted {
|
|
strongSelf.qrImageNode.alpha = 0.4
|
|
strongSelf.qrIconNode.alpha = 0.4
|
|
} else {
|
|
strongSelf.qrImageNode.layer.animateAlpha(from: strongSelf.qrImageNode.alpha, to: 1.0, duration: 0.2)
|
|
strongSelf.qrIconNode.layer.animateAlpha(from: strongSelf.qrIconNode.alpha, to: 1.0, duration: 0.2)
|
|
strongSelf.qrImageNode.alpha = 1.0
|
|
strongSelf.qrIconNode.alpha = 1.0
|
|
}
|
|
}
|
|
|
|
let textFont = Font.regular(16.0)
|
|
let secondaryTextColor = self.presentationData.theme.list.itemSecondaryTextColor
|
|
let url = urlForMode(self.mode)
|
|
switch self.mode {
|
|
case .receive:
|
|
self.textNode.attributedText = NSAttributedString(string: self.presentationData.strings.Wallet_Receive_ShareUrlInfo, font: textFont, textColor: secondaryTextColor)
|
|
self.buttonNode.title = self.presentationData.strings.Wallet_Receive_ShareAddress
|
|
self.secondaryButtonNode.setTitle(self.presentationData.strings.Wallet_Receive_CreateInvoice, with: Font.regular(17.0), with: self.presentationData.theme.list.itemAccentColor, for: .normal)
|
|
case .invoice:
|
|
self.textNode.attributedText = NSAttributedString(string: self.presentationData.strings.Wallet_Receive_ShareUrlInfo, font: textFont, textColor: secondaryTextColor, paragraphAlignment: .center)
|
|
self.buttonNode.title = self.presentationData.strings.Wallet_Receive_ShareInvoiceUrl
|
|
}
|
|
|
|
self.buttonNode.pressed = {
|
|
context.shareUrl(url)
|
|
}
|
|
self.secondaryButtonNode.addTarget(self, action: #selector(createInvoicePressed), forControlEvents: .touchUpInside)
|
|
}
|
|
|
|
override func didLoad() {
|
|
super.didLoad()
|
|
|
|
let addressGestureRecognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapAddressGesture(_:)))
|
|
addressGestureRecognizer.tapActionAtPoint = { point in
|
|
return .waitForSingleTap
|
|
}
|
|
self.urlTextNode.view.addGestureRecognizer(addressGestureRecognizer)
|
|
}
|
|
|
|
@objc func tapLongTapOrDoubleTapAddressGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
|
|
switch recognizer.state {
|
|
case .ended:
|
|
if let (gesture, _) = recognizer.lastRecognizedGestureAndLocation {
|
|
switch gesture {
|
|
case .longTap:
|
|
self.displayCopyContextMenu?(self, self.urlTextNode.frame, urlForMode(self.mode))
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
@objc private func qrPressed() {
|
|
shareInvoiceQrCode(context: self.context, invoice: urlForMode(self.mode))
|
|
}
|
|
|
|
@objc private func createInvoicePressed() {
|
|
self.openCreateInvoice?()
|
|
}
|
|
|
|
func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
|
self.validLayout = (layout, navigationHeight)
|
|
|
|
if self.urlTextNode.attributedText?.string.isEmpty ?? true {
|
|
var url = urlForMode(self.mode)
|
|
if case .receive = self.mode {
|
|
url = url + "?"
|
|
}
|
|
|
|
let addressFont: UIFont
|
|
let countRatio: CGFloat
|
|
if layout.size.width == 320.0 {
|
|
addressFont = Font.monospace(16.0)
|
|
countRatio = 0.0999
|
|
} else {
|
|
addressFont = Font.monospace(17.0)
|
|
countRatio = 0.0853
|
|
}
|
|
let count = min(url.count / 2, Int(ceil(min(layout.size.width, layout.size.height) * countRatio)))
|
|
let sliced = String(url.enumerated().map { $0 > 0 && $0 % count == 0 ? ["\n", $1] : [$1]}.joined())
|
|
|
|
self.urlTextNode.attributedText = NSAttributedString(string: sliced, font: addressFont, textColor: self.presentationData.theme.list.itemPrimaryTextColor, paragraphAlignment: .justified)
|
|
}
|
|
|
|
var insets = layout.insets(options: [])
|
|
insets.top += navigationHeight
|
|
let inset: CGFloat = 22.0
|
|
|
|
let buttonSideInset: CGFloat = 16.0
|
|
let bottomInset = insets.bottom + 10.0
|
|
let buttonWidth = layout.size.width - buttonSideInset * 2.0
|
|
let buttonHeight: CGFloat = 50.0
|
|
|
|
let textSize = self.textNode.updateLayout(CGSize(width: layout.size.width - inset * 2.0, height: CGFloat.greatestFiniteMagnitude))
|
|
|
|
let addressInset: CGFloat = 12.0
|
|
let urlTextSize = self.urlTextNode.updateLayout(CGSize(width: layout.size.width - addressInset * 2.0, height: CGFloat.greatestFiniteMagnitude))
|
|
|
|
var buttonOffset: CGFloat = 0.0
|
|
if let _ = self.secondaryButtonNode.attributedTitle(for: .normal) {
|
|
buttonOffset = -60.0
|
|
}
|
|
|
|
let imageSide: CGFloat = 215.0
|
|
let imageSize = CGSize(width: imageSide, height: imageSide)
|
|
|
|
let buttonTopEdge = layout.size.height - bottomInset - buttonHeight + buttonOffset
|
|
|
|
var topTextSpacing: CGFloat = 24.0
|
|
var imageSpacing: CGFloat = 20.0
|
|
var urlSpacing: CGFloat = 23.0
|
|
|
|
var contentHeight: CGFloat = insets.top
|
|
contentHeight += topTextSpacing + textSize.height
|
|
contentHeight += imageSpacing + imageSide
|
|
contentHeight += urlSpacing + urlTextSize.height
|
|
|
|
if contentHeight >= buttonTopEdge - 10.0 {
|
|
let factor: CGFloat = 0.5
|
|
topTextSpacing = floor(topTextSpacing * factor * 0.3)
|
|
imageSpacing = floor(imageSpacing * factor)
|
|
urlSpacing = floor(urlSpacing * factor)
|
|
}
|
|
|
|
let textFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - textSize.width) / 2.0), y: insets.top + topTextSpacing), size: textSize)
|
|
transition.updateFrame(node: self.textNode, frame: textFrame)
|
|
|
|
let makeImageLayout = self.qrImageNode.asyncLayout()
|
|
|
|
let imageApply = makeImageLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: nil))
|
|
|
|
let _ = imageApply()
|
|
|
|
let imageFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - imageSize.width) / 2.0), y: textFrame.maxY + imageSpacing), size: imageSize)
|
|
transition.updateFrame(node: self.qrImageNode, frame: imageFrame)
|
|
transition.updateFrame(node: self.qrButtonNode, frame: imageFrame)
|
|
|
|
if let qrCodeSize = self.qrCodeSize {
|
|
let (_, cutoutFrame, _) = qrCodeCutout(size: qrCodeSize, dimensions: imageSize, scale: nil)
|
|
self.qrIconNode.updateLayout(size: cutoutFrame.size)
|
|
transition.updateBounds(node: self.qrIconNode, bounds: CGRect(origin: CGPoint(), size: cutoutFrame.size))
|
|
transition.updatePosition(node: self.qrIconNode, position: imageFrame.center.offsetBy(dx: 0.0, dy: -1.0))
|
|
}
|
|
|
|
transition.updateFrame(node: self.urlTextNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - urlTextSize.width) / 2.0), y: imageFrame.maxY + urlSpacing), size: urlTextSize))
|
|
|
|
if let _ = self.secondaryButtonNode.attributedTitle(for: .normal) {
|
|
self.secondaryButtonNode.frame = CGRect(x: floor((layout.size.width - buttonWidth) / 2.0), y: layout.size.height - bottomInset - buttonHeight, width: buttonWidth, height: buttonHeight)
|
|
}
|
|
|
|
let buttonFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - buttonWidth) / 2.0), y: layout.size.height - bottomInset - buttonHeight + buttonOffset), size: CGSize(width: buttonWidth, height: buttonHeight))
|
|
transition.updateFrame(node: self.buttonNode, frame: buttonFrame)
|
|
let _ = self.buttonNode.updateLayout(width: buttonFrame.width, transition: transition)
|
|
}
|
|
}
|