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>
1192 lines
52 KiB
Swift
1192 lines
52 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import Display
|
|
import AccountContext
|
|
import SwiftSignalKit
|
|
import TelegramPresentationData
|
|
import PresentationDataUtils
|
|
import TelegramStringFormatting
|
|
import ComponentFlow
|
|
import ViewControllerComponent
|
|
import ResizableSheetComponent
|
|
import NavigationStackComponent
|
|
import BalancedTextComponent
|
|
import MultilineTextComponent
|
|
import ScrollComponent
|
|
import TextSelectionNode
|
|
import Pasteboard
|
|
import BundleIconComponent
|
|
import GlassBarButtonComponent
|
|
import ButtonComponent
|
|
import WalletContext
|
|
import WalletCardComponent
|
|
import AlertUI
|
|
|
|
fileprivate enum WalletTransferFinishResult {
|
|
case cancelled
|
|
case confirmed
|
|
}
|
|
|
|
private final class WalletTransferSigningTextComponent: Component {
|
|
typealias EnvironmentType = (Empty, ScrollChildEnvironment)
|
|
|
|
let theme: PresentationTheme
|
|
let strings: PresentationStrings
|
|
let text: NSAttributedString
|
|
let controller: () -> ViewController?
|
|
|
|
init(theme: PresentationTheme, strings: PresentationStrings, text: NSAttributedString, controller: @escaping () -> ViewController?) {
|
|
self.theme = theme
|
|
self.strings = strings
|
|
self.text = text
|
|
self.controller = controller
|
|
}
|
|
|
|
static func ==(lhs: WalletTransferSigningTextComponent, rhs: WalletTransferSigningTextComponent) -> Bool {
|
|
return lhs.theme === rhs.theme && lhs.strings === rhs.strings && lhs.text == rhs.text
|
|
}
|
|
|
|
final class View: UIView {
|
|
private let text = ComponentView<Empty>()
|
|
private var selection: TextSelectionNode?
|
|
private var component: WalletTransferSigningTextComponent?
|
|
|
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
if let selection = self.selection,
|
|
let result = selection.view.hitTest(self.convert(point, to: selection.view), with: event) {
|
|
return result
|
|
}
|
|
return super.hitTest(point, with: event)
|
|
}
|
|
|
|
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
|
if self.bounds.contains(point) { return true }
|
|
if let selection = self.selection {
|
|
return selection.view.hitTest(self.convert(point, to: selection.view), with: event) != nil
|
|
}
|
|
return false
|
|
}
|
|
|
|
override func didMoveToWindow() {
|
|
super.didMoveToWindow()
|
|
if self.window == nil { self.selection?.cancelSelection() }
|
|
}
|
|
|
|
private func removeSelection() {
|
|
guard let selection = self.selection else { return }
|
|
self.selection = nil
|
|
selection.cancelSelection()
|
|
selection.highlightAreaNode.view.removeFromSuperview()
|
|
selection.view.removeFromSuperview()
|
|
}
|
|
|
|
func update(component: WalletTransferSigningTextComponent, availableSize: CGSize, state: EmptyComponentState, transition: ComponentTransition) -> CGSize {
|
|
if let previous = self.component, previous != component {
|
|
self.removeSelection()
|
|
}
|
|
self.component = component
|
|
self.text.parentState = state
|
|
let textSize = self.text.update(
|
|
transition: transition,
|
|
component: AnyComponent(MultilineTextComponent(
|
|
text: .plain(component.text),
|
|
horizontalAlignment: .left,
|
|
maximumNumberOfLines: 0,
|
|
insets: UIEdgeInsets(top: 2.0, left: 0.0, bottom: 2.0, right: 0.0)
|
|
)),
|
|
environment: {},
|
|
containerSize: availableSize
|
|
)
|
|
if let textView = self.text.view as? MultilineTextComponent.View {
|
|
if textView.superview == nil { self.addSubview(textView) }
|
|
transition.setFrame(view: textView, frame: CGRect(origin: .zero, size: textSize))
|
|
|
|
if self.selection == nil && !component.text.string.isEmpty {
|
|
let accentColor = component.theme.actionSheet.controlAccentColor
|
|
let selection = TextSelectionNode(
|
|
theme: TextSelectionTheme(
|
|
selection: accentColor.withMultipliedAlpha(0.5),
|
|
knob: accentColor,
|
|
isDark: component.theme.overallDarkAppearance
|
|
),
|
|
strings: component.strings,
|
|
textNodeOrView: .view(textView),
|
|
updateIsActive: { _ in },
|
|
present: { [weak self] controller, arguments in
|
|
self?.component?.controller()?.presentInGlobalOverlay(controller, with: arguments)
|
|
},
|
|
rootView: { [weak self] in
|
|
return self?.component?.controller()?.displayNode.view
|
|
},
|
|
performAction: { text, action in
|
|
if action == .copy {
|
|
storeMessageTextInPasteboard(text.string, entities: nil)
|
|
}
|
|
}
|
|
)
|
|
selection.enableCopy = true
|
|
selection.enableLookup = false
|
|
selection.enableTranslate = false
|
|
selection.enableShare = false
|
|
selection.enableSpeak = false
|
|
selection.enableQuote = false
|
|
selection.enableAutomaticScrolling = false
|
|
selection.cancelSelectionOnOutsideTap = true
|
|
self.selection = selection
|
|
self.insertSubview(selection.highlightAreaNode.view, belowSubview: textView)
|
|
self.addSubview(selection.view)
|
|
}
|
|
if let selection = self.selection {
|
|
let needsLayout = selection.frame.size != textSize
|
|
selection.frame = CGRect(origin: .zero, size: textSize)
|
|
selection.highlightAreaNode.frame = selection.frame
|
|
if needsLayout { selection.updateLayout() }
|
|
}
|
|
}
|
|
return CGSize(width: availableSize.width, height: max(20.0, textSize.height))
|
|
}
|
|
}
|
|
|
|
func makeView() -> View {
|
|
return View(frame: .zero)
|
|
}
|
|
|
|
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
|
|
return view.update(component: self, availableSize: availableSize, state: state, transition: transition)
|
|
}
|
|
}
|
|
|
|
private final class WalletTransferSigningCardComponent: Component {
|
|
let theme: PresentationTheme
|
|
let strings: PresentationStrings
|
|
let content: WalletTransferPresentation.SigningContent
|
|
let controller: () -> ViewController?
|
|
let scrollViewUpdated: (UIScrollView?) -> Void
|
|
|
|
init(theme: PresentationTheme, strings: PresentationStrings, content: WalletTransferPresentation.SigningContent, controller: @escaping () -> ViewController?, scrollViewUpdated: @escaping (UIScrollView?) -> Void) {
|
|
self.theme = theme
|
|
self.strings = strings
|
|
self.content = content
|
|
self.controller = controller
|
|
self.scrollViewUpdated = scrollViewUpdated
|
|
}
|
|
|
|
static func ==(lhs: WalletTransferSigningCardComponent, rhs: WalletTransferSigningCardComponent) -> Bool {
|
|
return lhs.theme === rhs.theme && lhs.strings === rhs.strings && lhs.content == rhs.content
|
|
}
|
|
|
|
final class View: UIView {
|
|
private let scroll = ComponentView<Empty>()
|
|
private let scrollState = ScrollComponent<Empty>.ExternalState()
|
|
private let resetScroll = ActionSlot<CGPoint?>()
|
|
private let warningIcon = ComponentView<Empty>()
|
|
private let warningTitle = ComponentView<Empty>()
|
|
private let warningText = ComponentView<Empty>()
|
|
private let warningImage = UIImage(systemName: "exclamationmark.circle.fill")
|
|
private var content: WalletTransferPresentation.SigningContent?
|
|
|
|
func update(component: WalletTransferSigningCardComponent, availableSize: CGSize, state: EmptyComponentState, transition: ComponentTransition) -> CGSize {
|
|
transition.setBackgroundColor(view: self, color: component.theme.list.itemModalBlocksBackgroundColor)
|
|
transition.setCornerRadius(layer: self.layer, cornerRadius: 28.0)
|
|
self.clipsToBounds = true
|
|
let contentWidth = max(1.0, availableSize.width - 32.0)
|
|
let contentChanged = self.content != component.content
|
|
self.content = component.content
|
|
|
|
if case .binary = component.content {
|
|
self.scroll.view?.removeFromSuperview()
|
|
component.scrollViewUpdated(nil)
|
|
self.warningIcon.parentState = state
|
|
let iconSize = self.warningIcon.update(
|
|
transition: transition,
|
|
component: AnyComponent(Image(image: self.warningImage, tintColor: component.theme.list.itemDestructiveColor, size: CGSize(width: 16.0, height: 16.0))),
|
|
environment: {},
|
|
containerSize: CGSize(width: 16.0, height: 16.0)
|
|
)
|
|
if let iconView = self.warningIcon.view {
|
|
if iconView.superview == nil { self.addSubview(iconView) }
|
|
transition.setFrame(view: iconView, frame: CGRect(origin: CGPoint(x: 16.0, y: 18.0), size: iconSize))
|
|
}
|
|
self.warningTitle.parentState = state
|
|
let titleSize = self.warningTitle.update(
|
|
transition: transition,
|
|
component: AnyComponent(MultilineTextComponent(
|
|
text: .plain(NSAttributedString(string: component.strings.Wallet_Sign_BinaryContent, font: Font.regular(17.0), textColor: component.theme.list.itemPrimaryTextColor)),
|
|
maximumNumberOfLines: 0
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: max(1.0, contentWidth - 22.0), height: .greatestFiniteMagnitude)
|
|
)
|
|
if let titleView = self.warningTitle.view {
|
|
if titleView.superview == nil { self.addSubview(titleView) }
|
|
transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: 38.0, y: 14.0), size: titleSize))
|
|
}
|
|
self.warningText.parentState = state
|
|
let textSize = self.warningText.update(
|
|
transition: transition,
|
|
component: AnyComponent(MultilineTextComponent(
|
|
text: .plain(NSAttributedString(string: component.strings.Wallet_Sign_BinaryWarning, font: Font.regular(17.0), textColor: component.theme.list.itemPrimaryTextColor)),
|
|
maximumNumberOfLines: 0
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: contentWidth, height: .greatestFiniteMagnitude)
|
|
)
|
|
if let textView = self.warningText.view {
|
|
if textView.superview == nil { self.addSubview(textView) }
|
|
transition.setFrame(view: textView, frame: CGRect(origin: CGPoint(x: 16.0, y: 14.0 + titleSize.height + 2.0), size: textSize))
|
|
}
|
|
return CGSize(width: availableSize.width, height: 14.0 + titleSize.height + 2.0 + textSize.height + 14.0)
|
|
}
|
|
self.warningIcon.view?.removeFromSuperview()
|
|
self.warningTitle.view?.removeFromSuperview()
|
|
self.warningText.view?.removeFromSuperview()
|
|
|
|
let text = NSMutableAttributedString()
|
|
switch component.content {
|
|
case let .text(value):
|
|
text.append(NSAttributedString(string: value, font: Font.with(size: 17.0, design: .monospace), textColor: component.theme.list.itemPrimaryTextColor))
|
|
case let .message(groups):
|
|
let font = Font.with(size: 14.0, design: .monospace)
|
|
for (groupIndex, fields) in groups.enumerated() {
|
|
if groupIndex != 0 { text.append(NSAttributedString(string: "\n\n", font: font, textColor: component.theme.list.itemPrimaryTextColor)) }
|
|
for (fieldIndex, field) in fields.enumerated() {
|
|
if fieldIndex != 0 { text.append(NSAttributedString(string: "\n", font: font, textColor: component.theme.list.itemPrimaryTextColor)) }
|
|
text.append(NSAttributedString(string: field.name + ": ", font: font, textColor: component.theme.list.itemSecondaryTextColor))
|
|
text.append(NSAttributedString(string: field.value, font: font, textColor: component.theme.list.itemPrimaryTextColor))
|
|
}
|
|
}
|
|
case .binary:
|
|
break
|
|
}
|
|
let paragraphStyle = NSMutableParagraphStyle()
|
|
paragraphStyle.lineBreakMode = .byCharWrapping
|
|
text.addAttribute(.paragraphStyle, value: paragraphStyle, range: NSRange(location: 0, length: text.length))
|
|
|
|
let scrollComponent = AnyComponent(ScrollComponent<Empty>(
|
|
content: AnyComponent(WalletTransferSigningTextComponent(theme: component.theme, strings: component.strings, text: text, controller: component.controller)),
|
|
externalState: self.scrollState,
|
|
contentInsets: .zero,
|
|
contentOffsetUpdated: { _, _ in },
|
|
contentOffsetWillCommit: { _ in },
|
|
resetScroll: self.resetScroll
|
|
))
|
|
self.scroll.parentState = state
|
|
let _ = self.scroll.update(transition: transition, component: scrollComponent, environment: {}, containerSize: CGSize(width: contentWidth, height: 320.0))
|
|
let height = min(320.0, self.scrollState.contentHeight)
|
|
let scrollSize = self.scroll.update(transition: transition, component: scrollComponent, environment: {}, containerSize: CGSize(width: contentWidth, height: height))
|
|
if contentChanged { self.resetScroll.invoke(nil) }
|
|
if let scrollView = self.scroll.view {
|
|
if scrollView.superview == nil { self.addSubview(scrollView) }
|
|
transition.setFrame(view: scrollView, frame: CGRect(origin: CGPoint(x: 16.0, y: 14.0), size: scrollSize))
|
|
}
|
|
component.scrollViewUpdated(height >= 320.0 ? self.scroll.view as? UIScrollView : nil)
|
|
return CGSize(width: availableSize.width, height: scrollSize.height + 28.0)
|
|
}
|
|
}
|
|
|
|
func makeView() -> View {
|
|
return View(frame: .zero)
|
|
}
|
|
|
|
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
|
return view.update(component: self, availableSize: availableSize, state: state, transition: transition)
|
|
}
|
|
}
|
|
|
|
private final class WalletTransferSheetContent: Component {
|
|
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
|
|
|
let context: AccountContext
|
|
let request: WalletContext.TonConnectOperationRequest
|
|
let walletState: WalletContext.State?
|
|
let bottomInset: CGFloat
|
|
let infoPressed: () -> Void
|
|
let scrollViewUpdated: (UIScrollView?) -> Void
|
|
|
|
init(
|
|
context: AccountContext,
|
|
request: WalletContext.TonConnectOperationRequest,
|
|
walletState: WalletContext.State?,
|
|
bottomInset: CGFloat,
|
|
infoPressed: @escaping () -> Void,
|
|
scrollViewUpdated: @escaping (UIScrollView?) -> Void
|
|
) {
|
|
self.context = context
|
|
self.request = request
|
|
self.walletState = walletState
|
|
self.bottomInset = bottomInset
|
|
self.infoPressed = infoPressed
|
|
self.scrollViewUpdated = scrollViewUpdated
|
|
}
|
|
|
|
static func ==(lhs: WalletTransferSheetContent, rhs: WalletTransferSheetContent) -> Bool {
|
|
return lhs.request == rhs.request
|
|
&& lhs.walletState == rhs.walletState
|
|
&& lhs.bottomInset == rhs.bottomInset
|
|
}
|
|
|
|
final class View: UIView {
|
|
private let appIcon = ComponentView<Empty>()
|
|
private let title = ComponentView<Empty>()
|
|
private let domain = ComponentView<Empty>()
|
|
private let card = ComponentView<Empty>()
|
|
private let fee = ComponentView<Empty>()
|
|
private let dataTitle = ComponentView<Empty>()
|
|
private let signingCard = ComponentView<Empty>()
|
|
private let signingExplanation = ComponentView<Empty>()
|
|
private let signingSubmission = ComponentView<Empty>()
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
func update(
|
|
component: WalletTransferSheetContent,
|
|
availableSize: CGSize,
|
|
state: EmptyComponentState,
|
|
environment: Environment<EnvironmentType>,
|
|
transition: ComponentTransition
|
|
) -> CGSize {
|
|
let environment = environment[EnvironmentType.self].value
|
|
let theme = environment.theme
|
|
transition.setBackgroundColor(view: self, color: theme.list.modalPlainBackgroundColor)
|
|
|
|
let safeContentWidth = max(
|
|
0.0,
|
|
availableSize.width - environment.safeInsets.left - environment.safeInsets.right
|
|
)
|
|
let contentCenterX = environment.safeInsets.left + safeContentWidth / 2.0
|
|
let textWidth = max(1.0, safeContentWidth - 48.0)
|
|
let primaryTextColor = theme.actionSheet.primaryTextColor
|
|
let secondaryTextColor = theme.actionSheet.secondaryTextColor
|
|
let accentColor = theme.actionSheet.controlAccentColor
|
|
|
|
let presentation = WalletTransferPresentation(request: component.request, walletState: component.walletState)
|
|
if let signingContent = presentation.signingContent(strings: environment.strings, dateTimeFormat: environment.dateTimeFormat) {
|
|
self.appIcon.view?.isHidden = true
|
|
self.title.view?.isHidden = true
|
|
self.domain.view?.isHidden = true
|
|
self.card.view?.isHidden = true
|
|
transition.setBackgroundColor(view: self, color: theme.list.modalBlocksBackgroundColor)
|
|
|
|
let sideInset: CGFloat = 16.0
|
|
let cardWidth = max(1.0, safeContentWidth - sideInset * 2.0)
|
|
let cardX = environment.safeInsets.left + sideInset
|
|
var contentHeight: CGFloat = 90.0
|
|
func addText(_ text: String, view: ComponentView<Empty>, font: UIFont, topInset: CGFloat) {
|
|
guard !text.isEmpty else {
|
|
view.view?.isHidden = true
|
|
return
|
|
}
|
|
contentHeight += topInset
|
|
view.parentState = state
|
|
let size = view.update(
|
|
transition: transition,
|
|
component: AnyComponent(MultilineTextComponent(
|
|
text: .plain(NSAttributedString(string: text, font: font, textColor: secondaryTextColor)),
|
|
maximumNumberOfLines: 0
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: max(1.0, cardWidth - 32.0), height: .greatestFiniteMagnitude)
|
|
)
|
|
if let textView = view.view {
|
|
if textView.superview == nil { self.addSubview(textView) }
|
|
textView.isHidden = false
|
|
transition.setFrame(view: textView, frame: CGRect(origin: CGPoint(x: cardX + 16.0, y: contentHeight), size: size))
|
|
}
|
|
contentHeight += size.height
|
|
}
|
|
addText(environment.strings.Wallet_Sign_Data, view: self.dataTitle, font: Font.semibold(17.0), topInset: 0.0)
|
|
contentHeight += 12.0
|
|
self.signingCard.parentState = state
|
|
let cardSize = self.signingCard.update(
|
|
transition: transition,
|
|
component: AnyComponent(WalletTransferSigningCardComponent(
|
|
theme: theme,
|
|
strings: environment.strings,
|
|
content: signingContent,
|
|
controller: environment.controller,
|
|
scrollViewUpdated: component.scrollViewUpdated
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: cardWidth, height: availableSize.height)
|
|
)
|
|
if let cardView = self.signingCard.view {
|
|
if cardView.superview == nil { self.addSubview(cardView) }
|
|
transition.setFrame(view: cardView, frame: CGRect(origin: CGPoint(x: cardX, y: contentHeight), size: cardSize))
|
|
}
|
|
contentHeight += cardSize.height
|
|
addText(signingContent.explanation(strings: environment.strings) ?? "", view: self.signingExplanation, font: Font.regular(14.0), topInset: 10.0)
|
|
addText(presentation.submissionText(strings: environment.strings) ?? "", view: self.signingSubmission, font: Font.regular(14.0), topInset: 16.0)
|
|
addText(presentation.feeText(strings: environment.strings, dateTimeFormat: environment.dateTimeFormat), view: self.fee, font: Font.regular(14.0), topInset: 20.0)
|
|
return CGSize(width: availableSize.width, height: contentHeight + 16.0 + component.bottomInset)
|
|
}
|
|
self.dataTitle.view?.isHidden = true
|
|
self.signingCard.view?.removeFromSuperview()
|
|
component.scrollViewUpdated(nil)
|
|
self.signingExplanation.view?.isHidden = true
|
|
self.signingSubmission.view?.isHidden = true
|
|
self.appIcon.view?.isHidden = false
|
|
self.title.view?.isHidden = false
|
|
self.domain.view?.isHidden = false
|
|
self.card.view?.isHidden = false
|
|
self.fee.view?.isHidden = false
|
|
|
|
var contentHeight: CGFloat = 32.0
|
|
|
|
self.appIcon.parentState = state
|
|
let appIconSize = CGSize(width: 88.0, height: 88.0)
|
|
let _ = self.appIcon.update(
|
|
transition: transition,
|
|
component: AnyComponent(WalletConnectAppIconComponent(
|
|
context: component.context,
|
|
applicationName: component.request.applicationName,
|
|
icon: component.request.icon
|
|
)),
|
|
environment: {},
|
|
containerSize: appIconSize
|
|
)
|
|
if let appIconView = self.appIcon.view {
|
|
if appIconView.superview == nil {
|
|
self.addSubview(appIconView)
|
|
}
|
|
appIconView.clipsToBounds = true
|
|
transition.setCornerRadius(layer: appIconView.layer, cornerRadius: appIconSize.width * 0.5)
|
|
transition.setFrame(
|
|
view: appIconView,
|
|
frame: CGRect(
|
|
origin: CGPoint(x: floor(contentCenterX - appIconSize.width / 2.0), y: contentHeight),
|
|
size: appIconSize
|
|
)
|
|
)
|
|
}
|
|
contentHeight += appIconSize.height
|
|
contentHeight += 18.0
|
|
|
|
self.title.parentState = state
|
|
let titleSize = self.title.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(BalancedTextComponent(
|
|
text: .plain(NSAttributedString(
|
|
string: environment.strings.Wallet_Sign_ConfirmAction,
|
|
font: Font.bold(22.0),
|
|
textColor: primaryTextColor
|
|
)),
|
|
horizontalAlignment: .center,
|
|
maximumNumberOfLines: 0,
|
|
lineSpacing: 0.1
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: textWidth, height: availableSize.height)
|
|
)
|
|
if let titleView = self.title.view {
|
|
if titleView.superview == nil {
|
|
self.addSubview(titleView)
|
|
}
|
|
transition.setFrame(
|
|
view: titleView,
|
|
frame: CGRect(
|
|
origin: CGPoint(x: floor(contentCenterX - titleSize.width / 2.0), y: contentHeight),
|
|
size: titleSize
|
|
)
|
|
)
|
|
}
|
|
contentHeight += titleSize.height
|
|
contentHeight += 4.0
|
|
|
|
let domainItems: [AnyComponentWithIdentity<Empty>] = [AnyComponentWithIdentity(
|
|
id: "domain",
|
|
component: AnyComponent(Text(
|
|
text: component.request.domain,
|
|
font: Font.semibold(15.0),
|
|
color: accentColor
|
|
))
|
|
)]
|
|
self.domain.parentState = state
|
|
let domainSize = self.domain.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(HStack<Empty>(domainItems, spacing: 4.0)),
|
|
environment: {},
|
|
containerSize: CGSize(width: textWidth, height: 30.0)
|
|
)
|
|
if let domainView = self.domain.view {
|
|
if domainView.superview == nil {
|
|
self.addSubview(domainView)
|
|
}
|
|
transition.setFrame(
|
|
view: domainView,
|
|
frame: CGRect(
|
|
origin: CGPoint(x: floor(contentCenterX - domainSize.width / 2.0), y: contentHeight),
|
|
size: domainSize
|
|
)
|
|
)
|
|
}
|
|
contentHeight += domainSize.height
|
|
contentHeight += 25.0
|
|
|
|
let fiatCurrency = component.walletState?.fiat.selectedCurrency ?? .usd
|
|
let fiatRate = component.walletState?.fiat.selectedRate
|
|
let amountNanograms = presentation.amountNanograms
|
|
let amount = amountNanograms.flatMap { Int64($0) }
|
|
let cardWidth = min(361.0, max(1.0, safeContentWidth - 42.0))
|
|
self.card.parentState = state
|
|
let cardSize = self.card.update(
|
|
transition: transition,
|
|
component: AnyComponent(WalletTransferCardComponent(
|
|
amount: amount ?? 0,
|
|
recipient: presentation.recipient,
|
|
fiatCurrency: fiatCurrency,
|
|
fiatRate: fiatRate,
|
|
dateTimeFormat: environment.dateTimeFormat,
|
|
amountText: amount == nil ? formatTonConnectNanograms(amountNanograms ?? "", strings: environment.strings, dateTimeFormat: environment.dateTimeFormat) : nil,
|
|
recipientTitle: presentation.recipientTitle(strings: environment.strings),
|
|
infoPressed: component.infoPressed
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: cardWidth, height: availableSize.height)
|
|
)
|
|
if let cardView = self.card.view {
|
|
if cardView.superview == nil {
|
|
self.addSubview(cardView)
|
|
}
|
|
cardView.clipsToBounds = true
|
|
transition.setFrame(
|
|
view: cardView,
|
|
frame: CGRect(
|
|
origin: CGPoint(x: floor(contentCenterX - cardSize.width / 2.0), y: contentHeight),
|
|
size: cardSize
|
|
)
|
|
)
|
|
}
|
|
contentHeight += cardSize.height
|
|
contentHeight += 18.0
|
|
|
|
let feeText = presentation.feeText(strings: environment.strings, dateTimeFormat: environment.dateTimeFormat)
|
|
self.fee.parentState = state
|
|
let feeSize = self.fee.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(BalancedTextComponent(
|
|
text: .plain(NSAttributedString(
|
|
string: feeText,
|
|
font: Font.regular(14.0),
|
|
textColor: secondaryTextColor
|
|
)),
|
|
horizontalAlignment: .center,
|
|
maximumNumberOfLines: 0,
|
|
lineSpacing: 0.2
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: textWidth, height: availableSize.height)
|
|
)
|
|
if let feeView = self.fee.view {
|
|
if feeView.superview == nil {
|
|
self.addSubview(feeView)
|
|
}
|
|
transition.setFrame(
|
|
view: feeView,
|
|
frame: CGRect(
|
|
origin: CGPoint(x: floor(contentCenterX - feeSize.width / 2.0), y: contentHeight),
|
|
size: feeSize
|
|
)
|
|
)
|
|
}
|
|
contentHeight += feeSize.height
|
|
contentHeight += component.bottomInset
|
|
|
|
return CGSize(width: availableSize.width, height: contentHeight)
|
|
}
|
|
}
|
|
|
|
func makeView() -> View {
|
|
return View(frame: .zero)
|
|
}
|
|
|
|
func update(
|
|
view: View,
|
|
availableSize: CGSize,
|
|
state: EmptyComponentState,
|
|
environment: Environment<EnvironmentType>,
|
|
transition: ComponentTransition
|
|
) -> CGSize {
|
|
return view.update(
|
|
component: self,
|
|
availableSize: availableSize,
|
|
state: state,
|
|
environment: environment,
|
|
transition: transition
|
|
)
|
|
}
|
|
}
|
|
|
|
private final class WalletTransferActionsComponent: Component {
|
|
let theme: PresentationTheme
|
|
let strings: PresentationStrings
|
|
let confirmTitle: String
|
|
let isBusy: Bool
|
|
let isConfirming: Bool
|
|
let cancel: () -> Void
|
|
let confirm: () -> Void
|
|
|
|
init(
|
|
theme: PresentationTheme,
|
|
strings: PresentationStrings,
|
|
confirmTitle: String,
|
|
isBusy: Bool,
|
|
isConfirming: Bool,
|
|
cancel: @escaping () -> Void,
|
|
confirm: @escaping () -> Void
|
|
) {
|
|
self.theme = theme
|
|
self.strings = strings
|
|
self.confirmTitle = confirmTitle
|
|
self.isBusy = isBusy
|
|
self.isConfirming = isConfirming
|
|
self.cancel = cancel
|
|
self.confirm = confirm
|
|
}
|
|
|
|
static func ==(lhs: WalletTransferActionsComponent, rhs: WalletTransferActionsComponent) -> Bool {
|
|
return lhs.theme == rhs.theme
|
|
&& lhs.strings === rhs.strings
|
|
&& lhs.confirmTitle == rhs.confirmTitle
|
|
&& lhs.isBusy == rhs.isBusy
|
|
&& lhs.isConfirming == rhs.isConfirming
|
|
}
|
|
|
|
final class View: UIView {
|
|
private let cancelButton = ComponentView<Empty>()
|
|
private let confirmButton = ComponentView<Empty>()
|
|
|
|
private var component: WalletTransferActionsComponent?
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
func update(
|
|
component: WalletTransferActionsComponent,
|
|
availableSize: CGSize,
|
|
transition: ComponentTransition
|
|
) -> CGSize {
|
|
self.component = component
|
|
|
|
let buttonSpacing: CGFloat = 10.0
|
|
let height = min(52.0, availableSize.height)
|
|
let cancelButtonWidth = floorToScreenPixels((availableSize.width - buttonSpacing) / 2.0)
|
|
let confirmButtonWidth = availableSize.width - buttonSpacing - cancelButtonWidth
|
|
|
|
let cancelSize = self.cancelButton.update(
|
|
transition: transition,
|
|
component: AnyComponent(ButtonComponent(
|
|
background: ButtonComponent.Background(
|
|
style: .glass,
|
|
color: component.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.1),
|
|
foreground: component.theme.list.itemPrimaryTextColor,
|
|
pressedColor: component.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.16),
|
|
cornerRadius: 26.0
|
|
),
|
|
content: AnyComponentWithIdentity(
|
|
id: "cancel",
|
|
component: AnyComponent(Text(
|
|
text: component.strings.Common_Cancel,
|
|
font: Font.semibold(17.0),
|
|
color: component.theme.list.itemPrimaryTextColor
|
|
))
|
|
),
|
|
isEnabled: !component.isBusy,
|
|
action: { [weak self] in
|
|
self?.component?.cancel()
|
|
}
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: cancelButtonWidth, height: height)
|
|
)
|
|
if let cancelView = self.cancelButton.view {
|
|
if cancelView.superview == nil {
|
|
self.addSubview(cancelView)
|
|
}
|
|
transition.setFrame(view: cancelView, frame: CGRect(origin: .zero, size: cancelSize))
|
|
}
|
|
|
|
let confirmSize = self.confirmButton.update(
|
|
transition: transition,
|
|
component: AnyComponent(ButtonComponent(
|
|
background: ButtonComponent.Background(
|
|
style: .glass,
|
|
color: component.theme.list.itemCheckColors.fillColor,
|
|
foreground: component.theme.list.itemCheckColors.foregroundColor,
|
|
pressedColor: component.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9),
|
|
cornerRadius: 26.0
|
|
),
|
|
content: AnyComponentWithIdentity(
|
|
id: "confirm",
|
|
component: AnyComponent(Text(
|
|
text: component.confirmTitle,
|
|
font: Font.semibold(17.0),
|
|
color: component.theme.list.itemCheckColors.foregroundColor
|
|
))
|
|
),
|
|
isEnabled: !component.isBusy,
|
|
displaysProgress: component.isConfirming,
|
|
action: { [weak self] in
|
|
self?.component?.confirm()
|
|
}
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: confirmButtonWidth, height: height)
|
|
)
|
|
if let confirmView = self.confirmButton.view {
|
|
if confirmView.superview == nil {
|
|
self.addSubview(confirmView)
|
|
}
|
|
transition.setFrame(
|
|
view: confirmView,
|
|
frame: CGRect(
|
|
origin: CGPoint(x: cancelButtonWidth + buttonSpacing, y: 0.0),
|
|
size: confirmSize
|
|
)
|
|
)
|
|
}
|
|
|
|
return CGSize(width: availableSize.width, height: height)
|
|
}
|
|
}
|
|
|
|
func makeView() -> View {
|
|
return View(frame: .zero)
|
|
}
|
|
|
|
func update(
|
|
view: View,
|
|
availableSize: CGSize,
|
|
state: EmptyComponentState,
|
|
environment: Environment<Empty>,
|
|
transition: ComponentTransition
|
|
) -> CGSize {
|
|
return view.update(component: self, availableSize: availableSize, transition: transition)
|
|
}
|
|
}
|
|
|
|
private final class WalletTransferSheetComponent: CombinedComponent {
|
|
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
|
|
|
let context: AccountContext
|
|
let walletContext: WalletContext
|
|
let request: WalletContext.TonConnectOperationRequest
|
|
let confirm: (@escaping (Result<Void, WalletContext.WalletError>) -> Void) -> Void
|
|
|
|
init(
|
|
context: AccountContext,
|
|
walletContext: WalletContext,
|
|
request: WalletContext.TonConnectOperationRequest,
|
|
confirm: @escaping (@escaping (Result<Void, WalletContext.WalletError>) -> Void) -> Void
|
|
) {
|
|
self.context = context
|
|
self.walletContext = walletContext
|
|
self.request = request
|
|
self.confirm = confirm
|
|
}
|
|
|
|
static func ==(lhs: WalletTransferSheetComponent, rhs: WalletTransferSheetComponent) -> Bool {
|
|
return lhs.context === rhs.context
|
|
&& lhs.walletContext === rhs.walletContext
|
|
&& lhs.request == rhs.request
|
|
}
|
|
|
|
final class State: ComponentState {
|
|
private let disposables = DisposableSet()
|
|
private var isFinished = false
|
|
|
|
fileprivate let sheetExternalState = ResizableSheetComponent<EnvironmentType>.ExternalState()
|
|
fileprivate var walletState: WalletContext.State?
|
|
fileprivate var isAuthorizing = false
|
|
fileprivate var isConfirming = false
|
|
fileprivate var isPreviewPresented = false
|
|
|
|
fileprivate var isBusy: Bool {
|
|
return self.isAuthorizing || self.isConfirming
|
|
}
|
|
|
|
init(walletContext: WalletContext) {
|
|
super.init()
|
|
|
|
self.disposables.add((walletContext.state
|
|
|> deliverOnMainQueue).start(next: { [weak self] walletState in
|
|
guard let self, !self.isFinished else {
|
|
return
|
|
}
|
|
self.walletState = walletState
|
|
self.updated(transition: .easeInOut(duration: 0.25))
|
|
}))
|
|
}
|
|
|
|
deinit {
|
|
self.disposables.dispose()
|
|
}
|
|
|
|
func finish(
|
|
_ result: WalletTransferFinishResult,
|
|
getController: () -> ViewController?,
|
|
animated: Bool,
|
|
animateOut: ActionSlot<Action<Void>>?
|
|
) {
|
|
guard !self.isFinished, let controller = getController() as? WalletTransferScreen else {
|
|
return
|
|
}
|
|
self.isFinished = true
|
|
controller.finish(result, animated: animated, animateOut: animateOut)
|
|
}
|
|
|
|
func confirm(
|
|
component: WalletTransferSheetComponent,
|
|
getController: @escaping () -> ViewController?,
|
|
animateOut: ActionSlot<Action<Void>>
|
|
) {
|
|
guard !self.isFinished, !self.isAuthorizing, !self.isConfirming else {
|
|
return
|
|
}
|
|
self.isAuthorizing = true
|
|
self.updated(transition: .easeInOut(duration: 0.2))
|
|
|
|
self.isAuthorizing = false
|
|
self.isConfirming = true
|
|
getController()?.view.isUserInteractionEnabled = false
|
|
self.updated(transition: .easeInOut(duration: 0.2))
|
|
component.confirm({ [weak self] result in
|
|
Queue.mainQueue().async {
|
|
guard let self, !self.isFinished, let controller = getController() as? WalletTransferScreen, !controller.isDismissed else {
|
|
return
|
|
}
|
|
getController()?.view.isUserInteractionEnabled = true
|
|
switch result {
|
|
case .success:
|
|
self.finish(
|
|
.confirmed,
|
|
getController: getController,
|
|
animated: true,
|
|
animateOut: animateOut
|
|
)
|
|
case let .failure(error):
|
|
self.isConfirming = false
|
|
self.updated(transition: .easeInOut(duration: 0.2))
|
|
guard error != .authorizationCancelled else { return }
|
|
guard let controller = getController() else {
|
|
return
|
|
}
|
|
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
|
controller.present(textAlertController(
|
|
context: component.context,
|
|
title: nil,
|
|
text: presentationData.strings.Wallet_Sign_Error,
|
|
actions: [
|
|
TextAlertAction(
|
|
type: .defaultAction,
|
|
title: presentationData.strings.Common_OK,
|
|
action: {}
|
|
)
|
|
]
|
|
), in: .window(.root))
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func makeState() -> State {
|
|
return State(walletContext: self.walletContext)
|
|
}
|
|
|
|
static var body: Body {
|
|
let sheet = Child(ResizableSheetComponent<EnvironmentType>.self)
|
|
let animateOut = StoredActionSlot(Action<Void>.self)
|
|
|
|
return { context in
|
|
let component = context.component
|
|
let componentState = context.state
|
|
let environment = context.environment[EnvironmentType.self]
|
|
let controller = environment.controller
|
|
let theme = environment.theme.withModalBlocksBackground()
|
|
let isSigning = WalletTransferPresentation(request: component.request, walletState: componentState.walletState).isSigning
|
|
|
|
let dismiss: (Bool) -> Void = { [weak componentState] animated in
|
|
componentState?.finish(
|
|
.cancelled,
|
|
getController: controller,
|
|
animated: animated,
|
|
animateOut: animated ? animateOut : nil
|
|
)
|
|
}
|
|
|
|
let bottomInsets = ContainerViewLayout.concentricInsets(
|
|
bottomInset: environment.safeInsets.bottom,
|
|
innerDiameter: 52.0,
|
|
sideInset: 30.0
|
|
)
|
|
let contentBottomInset = bottomInsets.bottom + 52.0 + 16.0
|
|
|
|
let popPreview: () -> Void = { [weak componentState] in
|
|
guard let componentState, componentState.isPreviewPresented else {
|
|
return
|
|
}
|
|
componentState.isPreviewPresented = false
|
|
componentState.updated(transition: .spring(duration: 0.45))
|
|
}
|
|
|
|
var navigationItems: [AnyComponentWithIdentity<EnvironmentType>] = [
|
|
AnyComponentWithIdentity(
|
|
id: "transfer",
|
|
component: AnyComponent(WalletTransferSheetContent(
|
|
context: component.context,
|
|
request: component.request,
|
|
walletState: componentState.walletState,
|
|
bottomInset: contentBottomInset,
|
|
infoPressed: { [weak componentState] in
|
|
guard let componentState, !isSigning, !componentState.isPreviewPresented else {
|
|
return
|
|
}
|
|
componentState.isPreviewPresented = true
|
|
componentState.updated(transition: .spring(duration: 0.45))
|
|
},
|
|
scrollViewUpdated: { [weak componentState] scrollView in
|
|
componentState?.sheetExternalState.setTrackedScrollView(scrollView)
|
|
}
|
|
))
|
|
)
|
|
]
|
|
if componentState.isPreviewPresented && !isSigning {
|
|
navigationItems.append(AnyComponentWithIdentity(
|
|
id: "preview",
|
|
component: AnyComponent(WalletTransferPreviewComponent(
|
|
context: component.context,
|
|
request: component.request,
|
|
walletState: componentState.walletState,
|
|
bottomInset: contentBottomInset
|
|
))
|
|
))
|
|
}
|
|
|
|
let titleItem: AnyComponent<Empty>?
|
|
let rightItem: AnyComponent<Empty>?
|
|
if componentState.isPreviewPresented || isSigning {
|
|
titleItem = AnyComponent(VStack<Empty>([
|
|
AnyComponentWithIdentity(
|
|
id: "title",
|
|
component: AnyComponent(Text(
|
|
text: isSigning ? environment.strings.Wallet_Sign_SignData : environment.strings.Wallet_Sign_ConfirmAction,
|
|
font: Font.semibold(17.0),
|
|
color: theme.actionSheet.primaryTextColor
|
|
))
|
|
),
|
|
AnyComponentWithIdentity(
|
|
id: "domain",
|
|
component: AnyComponent(Text(
|
|
text: component.request.domain,
|
|
font: Font.regular(13.0),
|
|
color: theme.actionSheet.secondaryTextColor
|
|
))
|
|
)
|
|
], spacing: 0.0))
|
|
rightItem = AnyComponent(WalletTransferNavigationAppIconComponent(
|
|
context: component.context,
|
|
applicationName: component.request.applicationName,
|
|
icon: component.request.icon
|
|
))
|
|
} else {
|
|
titleItem = nil
|
|
rightItem = nil
|
|
}
|
|
|
|
let sheetComponent = sheet.update(
|
|
component: ResizableSheetComponent<EnvironmentType>(
|
|
content: AnyComponent<EnvironmentType>(NavigationStackComponent(
|
|
items: navigationItems,
|
|
clipContent: true,
|
|
requestPop: popPreview
|
|
)),
|
|
titleItem: titleItem,
|
|
leftItem: AnyComponent(GlassBarButtonComponent(
|
|
size: CGSize(width: 44.0, height: 44.0),
|
|
backgroundColor: nil,
|
|
isDark: theme.overallDarkAppearance,
|
|
state: .glass,
|
|
component: AnyComponentWithIdentity(
|
|
id: componentState.isPreviewPresented ? "back" : "close",
|
|
component: AnyComponent(BundleIconComponent(
|
|
name: componentState.isPreviewPresented ? "Navigation/Back" : "Navigation/Close",
|
|
tintColor: theme.chat.inputPanel.panelControlColor
|
|
))
|
|
),
|
|
action: { [weak componentState] _ in
|
|
guard let componentState else {
|
|
return
|
|
}
|
|
if componentState.isPreviewPresented {
|
|
popPreview()
|
|
} else if !componentState.isBusy {
|
|
dismiss(true)
|
|
}
|
|
}
|
|
)),
|
|
rightItem: rightItem,
|
|
hasTopEdgeEffect: false,
|
|
bottomItem: AnyComponent(WalletTransferActionsComponent(
|
|
theme: theme,
|
|
strings: environment.strings,
|
|
confirmTitle: isSigning ? environment.strings.Wallet_Sign_Sign : environment.strings.Wallet_Sign_Confirm,
|
|
isBusy: componentState.isBusy,
|
|
isConfirming: componentState.isConfirming,
|
|
cancel: {
|
|
dismiss(true)
|
|
},
|
|
confirm: { [weak componentState] in
|
|
componentState?.confirm(
|
|
component: component,
|
|
getController: controller,
|
|
animateOut: animateOut
|
|
)
|
|
}
|
|
)),
|
|
backgroundColor: .color(theme.list.plainBackgroundColor),
|
|
clipsContent: true,
|
|
externalState: componentState.sheetExternalState,
|
|
animateOut: animateOut
|
|
),
|
|
environment: {
|
|
environment
|
|
ResizableSheetComponentEnvironment(
|
|
theme: theme,
|
|
statusBarHeight: environment.statusBarHeight,
|
|
safeInsets: environment.safeInsets,
|
|
inputHeight: 0.0,
|
|
metrics: environment.metrics,
|
|
deviceMetrics: environment.deviceMetrics,
|
|
isDisplaying: environment.value.isVisible,
|
|
isCentered: environment.metrics.widthClass == .regular,
|
|
screenSize: context.availableSize,
|
|
regularMetricsSize: CGSize(width: 430.0, height: 900.0),
|
|
dismiss: { animated in
|
|
dismiss(animated)
|
|
}
|
|
)
|
|
},
|
|
availableSize: context.availableSize,
|
|
transition: context.transition
|
|
)
|
|
context.add(sheetComponent.position(CGPoint(
|
|
x: context.availableSize.width / 2.0,
|
|
y: context.availableSize.height / 2.0
|
|
)))
|
|
|
|
return context.availableSize
|
|
}
|
|
}
|
|
}
|
|
|
|
public final class WalletTransferScreen: ViewControllerComponentContainer {
|
|
private let cancelled: () -> Void
|
|
public var tonConnectClosed: (() -> Void)?
|
|
private var finishResult: WalletTransferFinishResult?
|
|
fileprivate var isDismissed = false
|
|
|
|
public override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
|
|
self.isDismissed = true
|
|
super.dismiss(animated: flag, completion: completion)
|
|
}
|
|
|
|
public init(
|
|
context: AccountContext,
|
|
walletContext: WalletContext,
|
|
request: WalletContext.TonConnectOperationRequest,
|
|
cancelled: @escaping () -> Void,
|
|
confirm: @escaping (@escaping (Result<Void, WalletContext.WalletError>) -> Void) -> Void
|
|
) {
|
|
self.cancelled = cancelled
|
|
|
|
super.init(
|
|
context: context,
|
|
component: WalletTransferSheetComponent(
|
|
context: context,
|
|
walletContext: walletContext,
|
|
request: request,
|
|
confirm: confirm
|
|
),
|
|
navigationBarAppearance: .none,
|
|
statusBarStyle: .ignore,
|
|
theme: .default
|
|
)
|
|
|
|
self.navigationPresentation = .flatModal
|
|
|
|
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
|
|
}
|
|
|
|
required public init(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
public override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
self.view.disablesInteractiveModalDismiss = true
|
|
}
|
|
|
|
fileprivate func finish(
|
|
_ result: WalletTransferFinishResult,
|
|
animated: Bool,
|
|
animateOut: ActionSlot<Action<Void>>?
|
|
) {
|
|
guard self.finishResult == nil else {
|
|
return
|
|
}
|
|
self.finishResult = result
|
|
|
|
let callback: () -> Void
|
|
switch result {
|
|
case .cancelled:
|
|
callback = self.cancelled
|
|
case .confirmed:
|
|
callback = {}
|
|
}
|
|
|
|
let dismissController: () -> Void = { [weak self] in
|
|
guard let self else {
|
|
callback()
|
|
return
|
|
}
|
|
self.dismiss(completion: {
|
|
callback()
|
|
self.tonConnectClosed?()
|
|
})
|
|
}
|
|
if animated, let animateOut {
|
|
animateOut.invoke(Action { _ in
|
|
dismissController()
|
|
})
|
|
} else if animated {
|
|
dismissController()
|
|
} else {
|
|
self.dismiss(animated: false, completion: {
|
|
callback()
|
|
self.tonConnectClosed?()
|
|
})
|
|
}
|
|
}
|
|
|
|
public func dismissAnimated() {
|
|
if let view = self.node.hostView.findTaggedView(
|
|
tag: ResizableSheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()
|
|
) as? ResizableSheetComponent<ViewControllerComponentContainer.Environment>.View {
|
|
view.dismissAnimated()
|
|
} else {
|
|
self.finish(.cancelled, animated: false, animateOut: nil)
|
|
}
|
|
}
|
|
}
|