import Foundation import UIKit import AppBundle import AsyncDisplayKit import Display import SolidRoundedButtonNode import SwiftSignalKit import MergeLists import AnimatedStickerNode import WalletCore private class WalletInfoTitleView: UIView, NavigationBarTitleView { private let buttonView: HighlightTrackingButton private let action: () -> Void init(action: @escaping () -> Void) { self.action = action self.buttonView = HighlightTrackingButton() super.init(frame: CGRect()) self.addSubview(self.buttonView) self.buttonView.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @objc private func buttonPressed() { self.action() } func animateLayoutTransition() { } func updateLayout(size: CGSize, clearBounds: CGRect, transition: ContainedViewLayoutTransition) { self.buttonView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)) } } public final class WalletInfoScreen: ViewController { private let context: WalletContext private let walletInfo: WalletInfo private let address: String private let enableDebugActions: Bool private var presentationData: WalletPresentationData private let _ready = Promise() override public var ready: Promise { return self._ready } public init(context: WalletContext, walletInfo: WalletInfo, address: String, enableDebugActions: Bool) { self.context = context self.walletInfo = walletInfo self.address = address self.enableDebugActions = enableDebugActions self.presentationData = context.presentationData let navigationBarTheme = NavigationBarTheme(buttonColor: .white, disabledButtonColor: .white, primaryTextColor: .white, backgroundColor: .clear, separatorColor: .clear, badgeBackgroundColor: self.presentationData.theme.navigationBar.badgeBackgroundColor, badgeStrokeColor: self.presentationData.theme.navigationBar.badgeStrokeColor, badgeTextColor: self.presentationData.theme.navigationBar.badgeTextColor) super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: navigationBarTheme, strings: NavigationBarStrings(back: self.presentationData.strings.Wallet_Navigation_Back, close: self.presentationData.strings.Wallet_Navigation_Close))) self.statusBar.statusBarStyle = .White self.navigationPresentation = .modalInLargeLayout self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) self.navigationBar?.intrinsicCanTransitionInline = false self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Wallet_Navigation_Back, style: .plain, target: nil, action: nil) self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: generateTintedImage(image: UIImage(bundleImageName: "Wallet/NavigationSettingsIcon"), color: .white), style: .plain, target: self, action: #selector(self.settingsPressed)) self.navigationItem.titleView = WalletInfoTitleView(action: { [weak self] in self?.scrollToTop?() }) self.scrollToTop = { [weak self] in (self?.displayNode as? WalletInfoScreenNode)?.scrollToTop() } } required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } @objc private func backPressed() { self.dismiss() } @objc private func settingsPressed() { self.push(walletSettingsController(context: self.context, walletInfo: self.walletInfo)) } override public func loadDisplayNode() { self.displayNode = WalletInfoScreenNode(context: self.context, presentationData: self.presentationData, walletInfo: self.walletInfo, address: self.address, sendAction: { [weak self] in guard let strongSelf = self else { return } let walletInfo = strongSelf.walletInfo guard let combinedState = (strongSelf.displayNode as! WalletInfoScreenNode).combinedState else { return } if (strongSelf.displayNode as! WalletInfoScreenNode).reloadingState { strongSelf.present(standardTextAlertController(theme: strongSelf.presentationData.theme.alert, title: nil, text: strongSelf.presentationData.strings.Wallet_Send_SyncInProgress, actions: [ TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Wallet_Alert_OK, action: { }) ]), in: .window(.root)) } else if !combinedState.pendingTransactions.isEmpty { strongSelf.present(standardTextAlertController(theme: strongSelf.presentationData.theme.alert, title: nil, text: strongSelf.presentationData.strings.Wallet_Send_TransactionInProgress, actions: [ TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Wallet_Alert_OK, action: { }) ]), in: .window(.root)) } else { var randomId: Int64 = 0 arc4random_buf(&randomId, 8) strongSelf.push(walletSendScreen(context: strongSelf.context, randomId: randomId, walletInfo: walletInfo)) } }, receiveAction: { [weak self] in guard let strongSelf = self else { return } let _ = (walletAddress(walletInfo: strongSelf.walletInfo, tonInstance: strongSelf.context.tonInstance) |> deliverOnMainQueue).start(next: { address in guard let strongSelf = self else { return } strongSelf.push(WalletReceiveScreen(context: strongSelf.context, mode: .receive(address: address))) }) }, openTransaction: { [weak self] transaction in guard let strongSelf = self else { return } strongSelf.push(WalletTransactionInfoScreen(context: strongSelf.context, walletInfo: strongSelf.walletInfo, walletTransaction: transaction, walletState: (strongSelf.displayNode as! WalletInfoScreenNode).statePromise.get(), enableDebugActions: strongSelf.enableDebugActions, decryptionKeyUpdated: { key in guard let strongSelf = self else { return } (strongSelf.displayNode as! WalletInfoScreenNode).updateTransactionDecryptionKey(key) })) }, present: { [weak self] c, a in guard let strongSelf = self else { return } strongSelf.present(c, in: .window(.root), with: a) }) self.displayNodeDidLoad() self._ready.set((self.displayNode as! WalletInfoScreenNode).contentReady.get()) } override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) (self.displayNode as! WalletInfoScreenNode).containerLayoutUpdated(layout: layout, navigationHeight: self.navigationHeight, transition: transition) } } final class WalletInfoBalanceNode: ASDisplayNode { let dateTimeFormat: WalletPresentationDateTimeFormat let balanceIntegralTextNode: ImmediateTextNode let balanceFractionalTextNode: ImmediateTextNode let balanceIconNode: AnimatedStickerNode private var balanceIconNodeIsStatic: Bool var balance: (String, UIColor) = (" ", .white) { didSet { let integralString = NSMutableAttributedString() let fractionalString = NSMutableAttributedString() if let range = self.balance.0.range(of: self.dateTimeFormat.decimalSeparator) { let integralPart = String(self.balance.0[.. CGFloat { let sideInset: CGFloat = 16.0 let balanceIconSpacing: CGFloat = scaleTransition * 0.0 + (1.0 - scaleTransition) * (-12.0) let balanceVerticalIconOffset: CGFloat = scaleTransition * (-2.0) + (1.0 - scaleTransition) * (-2.0) let balanceIntegralTextSize = self.balanceIntegralTextNode.updateLayout(CGSize(width: width - sideInset * 2.0, height: 200.0)) let balanceFractionalTextSize = self.balanceFractionalTextNode.updateLayout(CGSize(width: width - sideInset * 2.0, height: 200.0)) let balanceIconSize = CGSize(width: 50.0, height: 50.0) let integralScale: CGFloat = scaleTransition * 1.0 + (1.0 - scaleTransition) * 0.8 let fractionalScale: CGFloat = scaleTransition * 0.5 + (1.0 - scaleTransition) * 0.8 let balanceOrigin = CGPoint(x: floor((width - balanceIntegralTextSize.width * integralScale - balanceFractionalTextSize.width * fractionalScale - balanceIconSpacing + balanceIconSize.width / 2.0) / 2.0), y: 0.0) let balanceIntegralTextFrame = CGRect(origin: balanceOrigin, size: balanceIntegralTextSize) let apparentBalanceIntegralTextFrame = CGRect(origin: balanceIntegralTextFrame.origin, size: CGSize(width: balanceIntegralTextFrame.width * integralScale, height: balanceIntegralTextFrame.height * integralScale)) var balanceFractionalTextFrame = CGRect(origin: CGPoint(x: balanceIntegralTextFrame.maxX, y: balanceIntegralTextFrame.maxY - balanceFractionalTextSize.height), size: balanceFractionalTextSize) balanceFractionalTextFrame.origin.x -= (balanceFractionalTextFrame.width / 4.0) * scaleTransition + 0.25 * (balanceFractionalTextFrame.width / 2.0) * (1.0 - scaleTransition) balanceFractionalTextFrame.origin.y += balanceFractionalTextFrame.height * 0.5 * (0.8 - fractionalScale) let isBalanceEmpty = self.balance.0.isEmpty || self.balance.0 == " " let balanceIconFrame: CGRect if isBalanceEmpty { balanceIconFrame = CGRect(origin: CGPoint(x: floor((width - balanceIconSize.width) / 2.0), y: balanceIntegralTextFrame.midY - balanceIconSize.height / 2.0 + balanceVerticalIconOffset), size: balanceIconSize) } else { balanceIconFrame = CGRect(origin: CGPoint(x: apparentBalanceIntegralTextFrame.minX - balanceIconSize.width - balanceIconSpacing * integralScale, y: balanceIntegralTextFrame.midY - balanceIconSize.height / 2.0 + balanceVerticalIconOffset), size: balanceIconSize) } transition.updateFrameAsPositionAndBounds(node: self.balanceIntegralTextNode, frame: balanceIntegralTextFrame) transition.updateTransformScale(node: self.balanceIntegralTextNode, scale: integralScale) transition.updateFrameAsPositionAndBounds(node: self.balanceFractionalTextNode, frame: balanceFractionalTextFrame) transition.updateTransformScale(node: self.balanceFractionalTextNode, scale: fractionalScale) if !isBalanceEmpty != self.balanceIconNodeIsStatic { self.balanceIconNodeIsStatic = !isBalanceEmpty if isBalanceEmpty { if let path = getAppBundle().path(forResource: "WalletIntroLoading", ofType: "tgs") { self.balanceIconNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 120, height: 120, mode: .direct) } } else { if let path = getAppBundle().path(forResource: "WalletIntroStatic", ofType: "tgs") { self.balanceIconNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 120, height: 120, mode: .direct) } } } self.balanceIconNode.updateLayout(size: balanceIconFrame.size) transition.updateFrameAsPositionAndBounds(node: self.balanceIconNode, frame: balanceIconFrame) transition.updateTransformScale(node: self.balanceIconNode, scale: scaleTransition * 1.0 + (1.0 - scaleTransition) * 0.8) return balanceIntegralTextSize.height } } private final class WalletInfoHeaderNode: ASDisplayNode { var balance: Int64? var isRefreshing: Bool = false var timestamp: Int32? private let hasActions: Bool let balanceNode: WalletInfoBalanceNode let refreshNode: WalletRefreshNode let balanceSubtitleNode: ImmediateTextNode let balanceSubtitleIconNode: AnimatedStickerNode private let receiveButtonNode: SolidRoundedButtonNode private let receiveGramsButtonNode: SolidRoundedButtonNode private let sendButtonNode: SolidRoundedButtonNode private let headerBackgroundNode: ASDisplayNode private let headerCornerNode: ASImageNode init(presentationData: WalletPresentationData, hasActions: Bool, sendAction: @escaping () -> Void, receiveAction: @escaping () -> Void) { self.hasActions = hasActions self.balanceNode = WalletInfoBalanceNode(dateTimeFormat: presentationData.dateTimeFormat) self.balanceSubtitleNode = ImmediateTextNode() self.balanceSubtitleNode.displaysAsynchronously = false self.balanceSubtitleNode.attributedText = NSAttributedString(string: presentationData.strings.Wallet_Info_YourBalance, font: Font.regular(13), textColor: UIColor(white: 1.0, alpha: 0.6)) self.balanceSubtitleIconNode = AnimatedStickerNode() self.balanceSubtitleIconNode.isHidden = true if let path = getAppBundle().path(forResource: "WalletIntroStatic", ofType: "tgs") { self.balanceSubtitleIconNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 36, height: 36, mode: .direct) self.balanceSubtitleIconNode.visibility = true } self.balanceSubtitleNode.addSubnode(self.balanceSubtitleIconNode) self.headerBackgroundNode = ASDisplayNode() self.headerBackgroundNode.backgroundColor = .black self.headerCornerNode = ASImageNode() self.headerCornerNode.displaysAsynchronously = false self.headerCornerNode.displayWithoutProcessing = true self.headerCornerNode.image = generateImage(CGSize(width: 20.0, height: 10.0), rotatedContext: { size, context in context.setFillColor(UIColor.black.cgColor) context.fill(CGRect(origin: CGPoint(), size: size)) context.setBlendMode(.copy) context.setFillColor(UIColor.clear.cgColor) context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 20.0, height: 20.0))) })?.stretchableImage(withLeftCapWidth: 10, topCapHeight: 1) self.receiveButtonNode = SolidRoundedButtonNode(title: presentationData.strings.Wallet_Info_Receive, icon: generateTintedImage(image: UIImage(bundleImageName: "Wallet/ReceiveButtonIcon"), color: presentationData.theme.info.buttonTextColor), theme: SolidRoundedButtonTheme(backgroundColor: presentationData.theme.info.buttonBackgroundColor, foregroundColor: presentationData.theme.info.buttonTextColor), height: 50.0, cornerRadius: 10.0, gloss: false) self.receiveGramsButtonNode = SolidRoundedButtonNode(title: presentationData.strings.Wallet_Info_ReceiveGrams, icon: generateTintedImage(image: UIImage(bundleImageName: "Wallet/ReceiveButtonIcon"), color: presentationData.theme.info.buttonTextColor), theme: SolidRoundedButtonTheme(backgroundColor: presentationData.theme.info.buttonBackgroundColor, foregroundColor: presentationData.theme.info.buttonTextColor), height: 50.0, cornerRadius: 10.0, gloss: false) self.sendButtonNode = SolidRoundedButtonNode(title: presentationData.strings.Wallet_Info_Send, icon: generateTintedImage(image: UIImage(bundleImageName: "Wallet/SendButtonIcon"), color: presentationData.theme.info.buttonTextColor), theme: SolidRoundedButtonTheme(backgroundColor: presentationData.theme.info.buttonBackgroundColor, foregroundColor: presentationData.theme.info.buttonTextColor), height: 50.0, cornerRadius: 10.0, gloss: false) self.refreshNode = WalletRefreshNode(strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat) super.init() self.addSubnode(self.headerBackgroundNode) self.addSubnode(self.headerCornerNode) if hasActions { self.addSubnode(self.receiveButtonNode) self.addSubnode(self.receiveGramsButtonNode) self.addSubnode(self.sendButtonNode) } self.addSubnode(self.balanceNode) self.addSubnode(self.balanceSubtitleNode) self.addSubnode(self.refreshNode) self.receiveButtonNode.pressed = { receiveAction() } self.receiveGramsButtonNode.pressed = { receiveAction() } self.sendButtonNode.pressed = { sendAction() } } func update(size: CGSize, navigationHeight: CGFloat, offset: CGFloat, transition: ContainedViewLayoutTransition, isScrolling: Bool) { let sideInset: CGFloat = 16.0 let buttonHeight: CGFloat = 50.0 let balanceSubtitleSpacing: CGFloat = 0.0 let minOffset = navigationHeight let maxOffset = size.height let minHeaderOffset = minOffset let maxHeaderOffset = (minOffset + maxOffset) / 2.0 let effectiveOffset = max(offset, navigationHeight) let minButtonsOffset = maxOffset - buttonHeight * 2.0 - sideInset let maxButtonsOffset = maxOffset let buttonTransition: CGFloat = max(0.0, min(1.0, (effectiveOffset - minButtonsOffset) / (maxButtonsOffset - minButtonsOffset))) let buttonAlpha: CGFloat = buttonTransition let balanceSubtitleSize = self.balanceSubtitleNode.updateLayout(CGSize(width: size.width - sideInset * 2.0, height: 200.0)) let balanceSubtitleIconSize = CGSize(width: 18.0, height: 18.0) self.balanceSubtitleIconNode.frame = CGRect(origin: CGPoint(x: -balanceSubtitleIconSize.width - 2.0, y: -2.0), size: balanceSubtitleIconSize) self.balanceSubtitleIconNode.updateLayout(size: balanceSubtitleIconSize) let headerScaleTransition: CGFloat = max(0.0, min(1.0, (effectiveOffset - minHeaderOffset) / (maxHeaderOffset - minHeaderOffset))) let balanceHeight = self.balanceNode.update(width: size.width, scaleTransition: headerScaleTransition, transition: transition) let balanceSize = CGSize(width: size.width, height: balanceHeight) let maxHeaderScale: CGFloat = min(1.0, (size.width - 40.0) / balanceSize.width) let minHeaderScale: CGFloat = min(0.435, (size.width - 80.0 * 2.0) / balanceSize.width) let minHeaderHeight: CGFloat = balanceSize.height + balanceSubtitleSize.height + balanceSubtitleSpacing let minHeaderY = navigationHeight - 44.0 + floor((44.0 - minHeaderHeight) / 2.0) let maxHeaderY = floor((size.height - balanceSize.height) / 2.0 - balanceSubtitleSize.height) let headerPositionTransition: CGFloat = max(0.0, (effectiveOffset - minHeaderOffset) / (maxOffset - minHeaderOffset)) let headerY = headerPositionTransition * maxHeaderY + (1.0 - headerPositionTransition) * minHeaderY let headerScale = headerScaleTransition * maxHeaderScale + (1.0 - headerScaleTransition) * minHeaderScale let refreshSize = CGSize(width: 0.0, height: 0.0) transition.updateFrame(node: self.refreshNode, frame: CGRect(origin: CGPoint(x: floor((size.width - refreshSize.width) / 2.0), y: navigationHeight - 44.0 + floor((44.0 - refreshSize.height) / 2.0)), size: refreshSize)) transition.updateAlpha(node: self.refreshNode, alpha: headerScaleTransition, beginWithCurrentState: true) if self.isRefreshing { self.refreshNode.update(state: .refreshing) } else if self.balance == nil { self.refreshNode.update(state: .pullToRefresh(self.timestamp ?? 0, 0.0)) } else { let refreshOffset: CGFloat = 20.0 let refreshScaleTransition: CGFloat = max(0.0, (offset - maxOffset) / refreshOffset) self.refreshNode.update(state: .pullToRefresh(self.timestamp ?? 0, refreshScaleTransition * 0.1)) } let balanceFrame = CGRect(origin: CGPoint(x: 0.0, y: headerY), size: balanceSize) transition.updateFrame(node: self.balanceNode, frame: balanceFrame) transition.updateSublayerTransformScale(node: self.balanceNode, scale: headerScale) let balanceSubtitleOffset = headerScaleTransition * 27.0 + (1.0 - headerScaleTransition) * 9.0 let balanceSubtitleFrame = CGRect(origin: CGPoint(x: floor((size.width - balanceSubtitleSize.width) / 2.0), y: balanceFrame.midY + balanceSubtitleOffset), size: balanceSubtitleSize) transition.updateFrameAdditive(node: self.balanceSubtitleNode, frame: balanceSubtitleFrame) let headerHeight: CGFloat = 1000.0 transition.updateFrame(node: self.headerBackgroundNode, frame: CGRect(origin: CGPoint(x: -1.0, y: effectiveOffset - headerHeight), size: CGSize(width: size.width + 2.0, height: headerHeight))) transition.updateFrame(node: self.headerCornerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: effectiveOffset), size: CGSize(width: size.width, height: 10.0))) let buttonOffset = effectiveOffset let leftButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: buttonOffset - sideInset - buttonHeight), size: CGSize(width: floor((size.width - sideInset * 3.0) / 2.0), height: buttonHeight)) let sendButtonFrame = CGRect(origin: CGPoint(x: leftButtonFrame.maxX + sideInset, y: leftButtonFrame.minY), size: CGSize(width: size.width - leftButtonFrame.maxX - sideInset * 2.0, height: buttonHeight)) let fullButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: buttonOffset - sideInset - buttonHeight), size: CGSize(width: size.width - sideInset * 2.0, height: buttonHeight)) if let balance = self.balance, balance > 0 { self.receiveGramsButtonNode.isHidden = true self.receiveButtonNode.isHidden = false self.sendButtonNode.isHidden = false } else { if self.balance == nil { self.receiveGramsButtonNode.isHidden = false self.receiveButtonNode.isHidden = true self.sendButtonNode.isHidden = true } else { self.receiveGramsButtonNode.isHidden = false self.receiveButtonNode.isHidden = true self.sendButtonNode.isHidden = true } } if self.balance == nil { self.balanceNode.isHidden = false self.balanceSubtitleNode.isHidden = true self.refreshNode.isHidden = false } else { self.balanceNode.isHidden = false self.balanceSubtitleNode.isHidden = false self.refreshNode.isHidden = false } transition.updateFrame(node: self.receiveGramsButtonNode, frame: fullButtonFrame) transition.updateAlpha(node: self.receiveGramsButtonNode, alpha: buttonAlpha, beginWithCurrentState: true) transition.updateFrame(node: self.receiveButtonNode, frame: leftButtonFrame) transition.updateAlpha(node: self.receiveButtonNode, alpha: buttonAlpha, beginWithCurrentState: true) let _ = self.receiveGramsButtonNode.updateLayout(width: fullButtonFrame.width, transition: transition) let _ = self.receiveButtonNode.updateLayout(width: leftButtonFrame.width, transition: transition) transition.updateFrame(node: self.sendButtonNode, frame: sendButtonFrame) transition.updateAlpha(node: self.sendButtonNode, alpha: buttonAlpha, beginWithCurrentState: true) let _ = self.sendButtonNode.updateLayout(width: sendButtonFrame.width, transition: transition) } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if let result = self.sendButtonNode.hitTest(self.view.convert(point, to: self.sendButtonNode.view), with: event) { return result } if let result = self.receiveButtonNode.hitTest(self.view.convert(point, to: self.receiveButtonNode.view), with: event) { return result } if let result = self.receiveGramsButtonNode.hitTest(self.view.convert(point, to: self.receiveGramsButtonNode.view), with: event) { return result } return nil } func becameReady(animated: Bool) { if animated { self.sendButtonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.receiveGramsButtonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.receiveButtonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.balanceNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.balanceSubtitleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.refreshNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } self.balanceNode.isLoading = false } } private struct WalletInfoListTransaction { let deletions: [ListViewDeleteItem] let insertions: [ListViewInsertItem] let updates: [ListViewUpdateItem] } enum WalletInfoTransaction: Equatable { case completed(WalletTransaction) case pending(PendingWalletTransaction) } private enum WalletInfoListEntryId: Hashable { case empty case transaction(WalletTransactionId) case pendingTransaction(Data) } private enum WalletInfoListEntry: Equatable, Comparable, Identifiable { case empty(String, Bool) case transaction(Int, WalletInfoTransaction) var stableId: WalletInfoListEntryId { switch self { case .empty: return .empty case let .transaction(_, transaction): switch transaction { case let .completed(completed): return .transaction(completed.transactionId) case let .pending(pending): return .pendingTransaction(pending.bodyHash) } } } static func <(lhs: WalletInfoListEntry, rhs: WalletInfoListEntry) -> Bool { switch lhs { case .empty: switch rhs { case .empty: return false case .transaction: return true } case let .transaction(lhsIndex, _): switch rhs { case .empty: return false case let .transaction(rhsIndex, _): return lhsIndex < rhsIndex } } } func item(theme: WalletTheme, strings: WalletStrings, dateTimeFormat: WalletPresentationDateTimeFormat, action: @escaping (WalletInfoTransaction) -> Void, displayAddressContextMenu: @escaping (ASDisplayNode, CGRect) -> Void) -> ListViewItem { switch self { case let .empty(address, loading): return WalletInfoEmptyItem(theme: theme, strings: strings, address: address, loading: loading, displayAddressContextMenu: { node, frame in displayAddressContextMenu(node, frame) }) case let .transaction(_, transaction): return WalletInfoTransactionItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, walletTransaction: transaction, action: { action(transaction) }) } } } private func preparedTransition(from fromEntries: [WalletInfoListEntry], to toEntries: [WalletInfoListEntry], presentationData: WalletPresentationData, action: @escaping (WalletInfoTransaction) -> Void, displayAddressContextMenu: @escaping (ASDisplayNode, CGRect) -> Void) -> WalletInfoListTransaction { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, action: action, displayAddressContextMenu: displayAddressContextMenu), directionHint: nil) } let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, action: action, displayAddressContextMenu: displayAddressContextMenu), directionHint: nil) } return WalletInfoListTransaction(deletions: deletions, insertions: insertions, updates: updates) } private final class WalletInfoScreenNode: ViewControllerTracingNode { private let context: WalletContext private var presentationData: WalletPresentationData private let walletInfo: WalletInfo private let address: String private let openTransaction: (WalletInfoTransaction) -> Void private let present: (ViewController, Any?) -> Void private let hapticFeedback = HapticFeedback() private let headerNode: WalletInfoHeaderNode private let listNode: ListView private var enqueuedTransactions: [WalletInfoListTransaction] = [] private var validLayout: (ContainerViewLayout, CGFloat)? private let stateDisposable = MetaDisposable() private let transactionListDisposable = MetaDisposable() private let transactionDecryptionKey = Promise(nil) private var transactionDecryptionKeyDisposable: Disposable? private var listOffset: CGFloat? private(set) var reloadingState: Bool = false private var loadingMoreTransactions: Bool = false private var canLoadMoreTransactions: Bool = true fileprivate var combinedState: CombinedWalletState? private var currentEntries: [WalletInfoListEntry]? fileprivate let statePromise = Promise<(CombinedWalletState, Bool)>() private var isReady: Bool = false let contentReady = Promise() private var didSetContentReady = false private var updateTimestampTimer: SwiftSignalKit.Timer? private var pollCombinedStateDisposable: Disposable? private var watchCombinedStateDisposable: Disposable? private var refreshProgressDisposable: Disposable? init(context: WalletContext, presentationData: WalletPresentationData, walletInfo: WalletInfo, address: String, sendAction: @escaping () -> Void, receiveAction: @escaping () -> Void, openTransaction: @escaping (WalletInfoTransaction) -> Void, present: @escaping (ViewController, Any?) -> Void) { self.context = context self.presentationData = presentationData self.walletInfo = walletInfo self.address = address self.openTransaction = openTransaction self.present = present self.headerNode = WalletInfoHeaderNode(presentationData: presentationData, hasActions: true, sendAction: sendAction, receiveAction: receiveAction) self.listNode = ListView() self.listNode.verticalScrollIndicatorColor = UIColor(white: 0.0, alpha: 0.3) self.listNode.verticalScrollIndicatorFollowsOverscroll = true self.listNode.isHidden = false self.listNode.view.disablesInteractiveModalDismiss = true //self.listNode.keepMinimalScrollHeightWithTopInset = 0.0 super.init() self.backgroundColor = self.presentationData.theme.list.itemBlocksBackgroundColor self.addSubnode(self.listNode) self.addSubnode(self.headerNode) var canBeginRefresh = true var isScrolling = false self.listNode.beganInteractiveDragging = { isScrolling = true } self.listNode.endedInteractiveDragging = { isScrolling = false } self.listNode.updateFloatingHeaderOffset = { [weak self] offset, listTransition in guard let strongSelf = self, let (_, navigationHeight) = strongSelf.validLayout else { return } let headerHeight: CGFloat = navigationHeight + 260.0 strongSelf.listOffset = offset if strongSelf.isReady { if !strongSelf.reloadingState && canBeginRefresh && isScrolling { if offset >= headerHeight + 100.0 { canBeginRefresh = false strongSelf.hapticFeedback.impact() strongSelf.refreshTransactions() } } strongSelf.headerNode.update(size: strongSelf.headerNode.bounds.size, navigationHeight: navigationHeight, offset: offset, transition: listTransition, isScrolling: true) } } self.listNode.visibleBottomContentOffsetChanged = { [weak self] offset in guard let strongSelf = self else { return } guard case let .known(value) = offset, value < 100.0 else { return } if !strongSelf.loadingMoreTransactions && !strongSelf.reloadingState && strongSelf.canLoadMoreTransactions { strongSelf.loadMoreTransactions() } } self.listNode.didEndScrolling = { [weak self] in canBeginRefresh = true guard let strongSelf = self, let (_, _) = strongSelf.validLayout else { return } let topInset = strongSelf.listNode.insets.top - strongSelf.listNode.headerInsets.top switch strongSelf.listNode.visibleContentOffset() { case let .known(offset): if offset < topInset { if offset > topInset / 2.0 { strongSelf.scrollToHideHeader() } else { strongSelf.scrollToTop() } } default: break } } self.refreshTransactions() self.updateTimestampTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in guard let strongSelf = self, let combinedState = strongSelf.combinedState, !strongSelf.reloadingState else { return } strongSelf.headerNode.timestamp = Int32(clamping: combinedState.timestamp) }, queue: .mainQueue()) self.updateTimestampTimer?.start() let subject: CombinedWalletStateSubject = .wallet(walletInfo) let watchCombinedStateSignal = context.storage.watchWalletRecords() |> map { records -> CombinedWalletState? in for record in records { switch record.info { case let .ready(itemInfo, _, state): if itemInfo.publicKey == walletInfo.publicKey { return state } case .imported: break } } return nil } |> distinctUntilChanged let tonInstance = self.context.tonInstance let decryptedWalletState = combineLatest(queue: .mainQueue(), watchCombinedStateSignal, self.transactionDecryptionKey.get() ) |> mapToSignal { maybeState, decryptionKey -> Signal in guard let state = maybeState, let decryptionKey = decryptionKey else { return .single(maybeState) } return decryptWalletTransactions(decryptionKey: decryptionKey, transactions: state.topTransactions, tonInstance: tonInstance) |> `catch` { _ -> Signal<[WalletTransaction], NoError> in return .single(state.topTransactions) } |> map { transactions -> CombinedWalletState? in return state.withTopTransactions(transactions) } } self.watchCombinedStateDisposable = (decryptedWalletState |> deliverOnMainQueue).start(next: { [weak self] state in guard let strongSelf = self, let state = state else { return } if state.pendingTransactions != strongSelf.combinedState?.pendingTransactions || state.timestamp != strongSelf.combinedState?.timestamp { if !strongSelf.reloadingState { strongSelf.updateCombinedState(combinedState: state, isUpdated: true) } } }) let pollCombinedState: Signal = ( getCombinedWalletState(storage: context.storage, subject: subject, tonInstance: context.tonInstance, onlyCached: false) |> ignoreValues |> `catch` { _ -> Signal in return .complete() } |> then( Signal.complete() |> delay(5.0, queue: .mainQueue()) ) ) |> restart self.pollCombinedStateDisposable = (pollCombinedState |> deliverOnMainQueue).start() self.refreshProgressDisposable = (context.tonInstance.syncProgress |> deliverOnMainQueue).start(next: { [weak self] progress in guard let strongSelf = self else { return } strongSelf.headerNode.refreshNode.refreshProgress = progress if strongSelf.headerNode.isRefreshing, strongSelf.isReady, let (_, _) = strongSelf.validLayout { strongSelf.headerNode.refreshNode.update(state: .refreshing) } }) self.transactionDecryptionKeyDisposable = (self.transactionDecryptionKey.get() |> deliverOnMainQueue).start(next: { [weak self] value in guard let strongSelf = self else { return } if let value = value, let currentEntries = strongSelf.currentEntries { var encryptedTransactions: [WalletTransactionId: WalletTransaction] = [:] for entry in currentEntries { switch entry { case .empty: break case let .transaction(_, transaction): switch transaction { case let .completed(transaction): var isEncrypted = false if let inMessage = transaction.inMessage { switch inMessage.contents { case .encryptedText: isEncrypted = true default: break } } for outMessage in transaction.outMessages { switch outMessage.contents { case .encryptedText: isEncrypted = true default: break } } if isEncrypted { encryptedTransactions[transaction.transactionId] = transaction } case .pending: break } } } if !encryptedTransactions.isEmpty { let _ = (decryptWalletTransactions(decryptionKey: value, transactions: Array(encryptedTransactions.values), tonInstance: strongSelf.context.tonInstance) |> deliverOnMainQueue).start(next: { decryptedTransactions in guard let strongSelf = self else { return } var decryptedTransactionMap: [WalletTransactionId: WalletTransaction] = [:] for transaction in decryptedTransactions { decryptedTransactionMap[transaction.transactionId] = transaction } var updatedEntries: [WalletInfoListEntry] = [] for entry in currentEntries { switch entry { case .empty: updatedEntries.append(entry) case let .transaction(index, transaction): switch transaction { case .pending: updatedEntries.append(entry) case let .completed(transaction): if let decryptedTransaction = decryptedTransactionMap[transaction.transactionId] { updatedEntries.append(.transaction(index, .completed(decryptedTransaction))) } else { updatedEntries.append(entry) } } } } strongSelf.replaceEntries(updatedEntries) }) } } }) } deinit { self.stateDisposable.dispose() self.transactionListDisposable.dispose() self.updateTimestampTimer?.invalidate() self.pollCombinedStateDisposable?.dispose() self.watchCombinedStateDisposable?.dispose() self.refreshProgressDisposable?.dispose() self.transactionDecryptionKeyDisposable?.dispose() } func updateTransactionDecryptionKey(_ key: WalletTransactionDecryptionKey) { self.transactionDecryptionKey.set(.single(key)) } func scrollToHideHeader() { guard let (_, navigationHeight) = self.validLayout else { return } let _ = self.listNode.scrollToOffsetFromTop(self.headerNode.frame.maxY - navigationHeight) } func scrollToTop() { if !self.listNode.scrollToOffsetFromTop(0.0) { self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Spring(duration: 0.4), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) } } func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) { let isFirstLayout = self.validLayout == nil self.validLayout = (layout, navigationHeight) let headerHeight: CGFloat = navigationHeight + 260.0 let topInset: CGFloat = headerHeight let visualHeaderHeight: CGFloat let visualHeaderOffset: CGFloat if !self.isReady { visualHeaderHeight = layout.size.height visualHeaderOffset = visualHeaderHeight } else { visualHeaderHeight = headerHeight visualHeaderOffset = self.listOffset ?? 0.0 } let visualListOffset = visualHeaderHeight - headerHeight let headerFrame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: visualHeaderHeight)) transition.updateFrame(node: self.headerNode, frame: headerFrame) self.headerNode.update(size: headerFrame.size, navigationHeight: navigationHeight, offset: visualHeaderOffset, transition: transition, isScrolling: false) transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(x: 0.0, y: visualListOffset), size: layout.size)) let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: topInset, left: 0.0, bottom: layout.intrinsicInsets.bottom, right: 0.0), headerInsets: UIEdgeInsets(top: navigationHeight, left: 0.0, bottom: layout.intrinsicInsets.bottom, right: 0.0), scrollIndicatorInsets: UIEdgeInsets(top: topInset + 3.0, left: 0.0, bottom: layout.intrinsicInsets.bottom, right: 0.0), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) if isFirstLayout { while !self.enqueuedTransactions.isEmpty { self.dequeueTransaction() } } } private func refreshTransactions() { self.transactionListDisposable.set(nil) self.loadingMoreTransactions = true self.reloadingState = true self.updateStatePromise() self.headerNode.isRefreshing = true self.headerNode.refreshNode.refreshProgress = 0.0 let subject: CombinedWalletStateSubject = .wallet(self.walletInfo) let transactionDecryptionKey = self.transactionDecryptionKey let tonInstance = self.context.tonInstance let processedWalletState = getCombinedWalletState(storage: self.context.storage, subject: subject, tonInstance: self.context.tonInstance, onlyCached: false) |> mapToSignal { state -> Signal in return transactionDecryptionKey.get() |> castError(GetCombinedWalletStateError.self) |> take(1) |> mapToSignal { decryptionKey -> Signal in guard let decryptionKey = decryptionKey else { return .single(state) } switch state { case let .cached(value): if let value = value { return decryptWalletState(decryptionKey: decryptionKey, state: value, tonInstance: tonInstance) |> map { decryptedState -> CombinedWalletStateResult in return .cached(decryptedState) } |> `catch` { _ -> Signal in return .single(state) } } else { return .single(state) } case let .updated(value): return decryptWalletState(decryptionKey: decryptionKey, state: value, tonInstance: tonInstance) |> map { decryptedState -> CombinedWalletStateResult in return .updated(decryptedState) } |> `catch` { _ -> Signal in return .single(state) } } } } self.stateDisposable.set((processedWalletState |> deliverOnMainQueue).start(next: { [weak self] value in guard let strongSelf = self else { return } let combinedState: CombinedWalletState? var isUpdated = false switch value { case let .cached(state): if strongSelf.combinedState != nil { return } combinedState = state case let .updated(state): isUpdated = true combinedState = state } strongSelf.updateCombinedState(combinedState: combinedState, isUpdated: isUpdated) }, error: { [weak self] error in guard let strongSelf = self else { return } strongSelf.reloadingState = false strongSelf.updateStatePromise() if let combinedState = strongSelf.combinedState { strongSelf.headerNode.timestamp = Int32(clamping: combinedState.timestamp) } if strongSelf.isReady, let (_, navigationHeight) = strongSelf.validLayout { strongSelf.headerNode.update(size: strongSelf.headerNode.bounds.size, navigationHeight: navigationHeight, offset: strongSelf.listOffset ?? 0.0, transition: .immediate, isScrolling: false) } strongSelf.loadingMoreTransactions = false strongSelf.canLoadMoreTransactions = false strongSelf.headerNode.isRefreshing = false if strongSelf.isReady, let (_, navigationHeight) = strongSelf.validLayout { strongSelf.headerNode.update(size: strongSelf.headerNode.bounds.size, navigationHeight: navigationHeight, offset: strongSelf.listOffset ?? 0.0, transition: .animated(duration: 0.2, curve: .easeInOut), isScrolling: false) } if !strongSelf.didSetContentReady { strongSelf.didSetContentReady = true strongSelf.contentReady.set(.single(true)) } let text: String switch error { case .generic: text = strongSelf.presentationData.strings.Wallet_Info_RefreshErrorText case .network: text = strongSelf.presentationData.strings.Wallet_Info_RefreshErrorNetworkText } strongSelf.present(standardTextAlertController(theme: strongSelf.presentationData.theme.alert, title: strongSelf.presentationData.strings.Wallet_Info_RefreshErrorTitle, text: text, actions: [ TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Wallet_Alert_OK, action: { }) ], actionLayout: .vertical), nil) })) } private func updateCombinedState(combinedState: CombinedWalletState?, isUpdated: Bool) { self.combinedState = combinedState if let combinedState = combinedState { self.headerNode.balanceNode.balance = (formatBalanceText(max(0, combinedState.walletState.effectiveAvailableBalance), decimalSeparator: self.presentationData.dateTimeFormat.decimalSeparator), .white) if let unlockedBalance = combinedState.walletState.unlockedBalance { let lockedBalance = combinedState.walletState.totalBalance - unlockedBalance let balanceText = formatBalanceText(max(0, lockedBalance), decimalSeparator: self.presentationData.dateTimeFormat.decimalSeparator) let string = NSMutableAttributedString() string.append(NSAttributedString(string: "\(balanceText)", font: Font.semibold(13), textColor: .white)) string.append(NSAttributedString(string: " locked", font: Font.regular(13), textColor: .white)) self.headerNode.balanceSubtitleNode.attributedText = string self.headerNode.balanceSubtitleIconNode.isHidden = false } else { self.headerNode.balanceSubtitleNode.attributedText = NSAttributedString(string: self.presentationData.strings.Wallet_Info_YourBalance, font: Font.regular(13), textColor: UIColor(white: 1.0, alpha: 0.6)) self.headerNode.balanceSubtitleIconNode.isHidden = true } self.headerNode.balance = max(0, combinedState.walletState.effectiveAvailableBalance) if self.isReady, let (layout, navigationHeight) = self.validLayout { self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate) } if isUpdated { self.reloadingState = false } self.headerNode.timestamp = Int32(clamping: combinedState.timestamp) if self.isReady, let (_, navigationHeight) = self.validLayout { self.headerNode.update(size: self.headerNode.bounds.size, navigationHeight: navigationHeight, offset: self.listOffset ?? 0.0, transition: .immediate, isScrolling: false) } var updatedTransactions: [WalletTransaction] = combinedState.topTransactions if let currentEntries = self.currentEntries { var existingIds = Set() for transaction in updatedTransactions { existingIds.insert(.transaction(transaction.transactionId)) } for entry in currentEntries { switch entry { case let .transaction(_, transaction): switch transaction { case let .completed(transaction): if !existingIds.contains(.transaction(transaction.transactionId)) { existingIds.insert(.transaction(transaction.transactionId)) updatedTransactions.append(transaction) } case .pending: break } default: break } } } self.transactionsLoaded(isReload: true, isEmpty: false, transactions: updatedTransactions, pendingTransactions: combinedState.pendingTransactions) if isUpdated { self.headerNode.isRefreshing = false } if self.isReady, let (_, navigationHeight) = self.validLayout { self.headerNode.update(size: self.headerNode.bounds.size, navigationHeight: navigationHeight, offset: self.listOffset ?? 0.0, transition: .animated(duration: 0.2, curve: .easeInOut), isScrolling: false) } } else { self.transactionsLoaded(isReload: true, isEmpty: true, transactions: [], pendingTransactions: []) } let wasReady = self.isReady self.isReady = true if self.isReady && !wasReady { if let (layout, navigationHeight) = self.validLayout { self.headerNode.update(size: self.headerNode.bounds.size, navigationHeight: navigationHeight, offset: layout.size.height, transition: .immediate, isScrolling: false) } self.becameReady(animated: self.didSetContentReady) } if !self.didSetContentReady { self.didSetContentReady = true self.contentReady.set(.single(true)) } self.updateStatePromise() } private func updateStatePromise() { if let combinedState = self.combinedState { self.statePromise.set(.single((combinedState, self.reloadingState))) } } private func loadMoreTransactions() { if self.loadingMoreTransactions { return } self.loadingMoreTransactions = true var lastTransactionId: WalletTransactionId? if let last = self.currentEntries?.last { switch last { case let .transaction(_, transaction): switch transaction { case let .completed(completed): lastTransactionId = completed.transactionId case .pending: break } case .empty: break } } let transactionDecryptionKey = self.transactionDecryptionKey let tonInstance = self.context.tonInstance let requestTransactions = walletAddress(walletInfo: self.walletInfo, tonInstance: self.context.tonInstance) |> castError(GetWalletTransactionsError.self) |> mapToSignal { address -> Signal<[WalletTransaction], GetWalletTransactionsError> in getWalletTransactions(address: address, previousId: lastTransactionId, tonInstance: tonInstance) } let processedTransactions = requestTransactions |> mapToSignal { transactions -> Signal<[WalletTransaction], GetWalletTransactionsError> in return transactionDecryptionKey.get() |> castError(GetWalletTransactionsError.self) |> take(1) |> mapToSignal { decryptionKey -> Signal<[WalletTransaction], GetWalletTransactionsError> in guard let decryptionKey = decryptionKey else { return .single(transactions) } return decryptWalletTransactions(decryptionKey: decryptionKey, transactions: transactions, tonInstance: tonInstance) |> `catch` { _ -> Signal<[WalletTransaction], GetWalletTransactionsError> in return .single(transactions) } } } self.transactionListDisposable.set((processedTransactions |> deliverOnMainQueue).start(next: { [weak self] transactions in guard let strongSelf = self else { return } strongSelf.transactionsLoaded(isReload: false, isEmpty: false, transactions: transactions, pendingTransactions: []) }, error: { _ in })) } private func transactionsLoaded(isReload: Bool, isEmpty: Bool, transactions: [WalletTransaction], pendingTransactions: [PendingWalletTransaction]) { if !isEmpty { self.loadingMoreTransactions = false self.canLoadMoreTransactions = transactions.count > 2 } var updatedEntries: [WalletInfoListEntry] = [] if isReload { var existingIds = Set() for transaction in pendingTransactions { if !existingIds.contains(.pendingTransaction(transaction.bodyHash)) { existingIds.insert(.pendingTransaction(transaction.bodyHash)) updatedEntries.append(.transaction(updatedEntries.count, .pending(transaction))) } } for transaction in transactions { if !existingIds.contains(.transaction(transaction.transactionId)) { existingIds.insert(.transaction(transaction.transactionId)) updatedEntries.append(.transaction(updatedEntries.count, .completed(transaction))) } } if updatedEntries.isEmpty { updatedEntries.append(.empty(self.address, isEmpty)) } } else { updatedEntries = self.currentEntries ?? [] updatedEntries = updatedEntries.filter { entry in if case .empty = entry { return false } else { return true } } var existingIds = Set() for entry in updatedEntries { switch entry { case .transaction: existingIds.insert(entry.stableId) case .empty: break } } for transaction in transactions { if !existingIds.contains(.transaction(transaction.transactionId)) { existingIds.insert(.transaction(transaction.transactionId)) updatedEntries.append(.transaction(updatedEntries.count, .completed(transaction))) } } if updatedEntries.isEmpty { updatedEntries.append(.empty(self.address, false)) } } self.replaceEntries(updatedEntries) } private func replaceEntries(_ updatedEntries: [WalletInfoListEntry]) { let transaction = preparedTransition(from: self.currentEntries ?? [], to: updatedEntries, presentationData: self.presentationData, action: { [weak self] transaction in guard let strongSelf = self else { return } strongSelf.openTransaction(transaction) }, displayAddressContextMenu: { [weak self] node, frame in guard let strongSelf = self else { return } let address = strongSelf.address let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text(title: strongSelf.presentationData.strings.Wallet_ContextMenuCopy, accessibilityLabel: strongSelf.presentationData.strings.Wallet_ContextMenuCopy), action: { UIPasteboard.general.string = address })]) strongSelf.present(contextMenuController, ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in if let strongSelf = self { return (node, frame.insetBy(dx: 0.0, dy: -2.0), strongSelf, strongSelf.view.bounds) } else { return nil } })) }) self.currentEntries = updatedEntries self.enqueuedTransactions.append(transaction) self.dequeueTransaction() } private func dequeueTransaction() { guard let _ = self.validLayout, let transaction = self.enqueuedTransactions.first else { return } self.enqueuedTransactions.remove(at: 0) var options = ListViewDeleteAndInsertOptions() options.insert(.Synchronous) options.insert(.PreferSynchronousResourceLoading) options.insert(.PreferSynchronousDrawing) self.listNode.transaction(deleteIndices: transaction.deletions, insertIndicesAndItems: transaction.insertions, updateIndicesAndItems: transaction.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { _ in }) } private func becameReady(animated: Bool) { self.listNode.isHidden = false self.headerNode.becameReady(animated: animated) if let (layout, navigationHeight) = self.validLayout { self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: animated ? .animated(duration: 0.5, curve: .spring) : .immediate) } } } private final class WalletApplicationSplashScreenNode: ASDisplayNode { private let headerBackgroundNode: ASDisplayNode private let headerCornerNode: ASImageNode private var isDismissed = false private var validLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)? init(theme: WalletTheme) { self.headerBackgroundNode = ASDisplayNode() self.headerBackgroundNode.backgroundColor = .black self.headerCornerNode = ASImageNode() self.headerCornerNode.displaysAsynchronously = false self.headerCornerNode.displayWithoutProcessing = true self.headerCornerNode.image = generateImage(CGSize(width: 20.0, height: 10.0), rotatedContext: { size, context in context.setFillColor(UIColor.black.cgColor) context.fill(CGRect(origin: CGPoint(), size: size)) context.setBlendMode(.copy) context.setFillColor(UIColor.clear.cgColor) context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 20.0, height: 20.0))) })?.stretchableImage(withLeftCapWidth: 10, topCapHeight: 1) super.init() self.backgroundColor = theme.list.itemBlocksBackgroundColor self.addSubnode(self.headerBackgroundNode) self.addSubnode(self.headerCornerNode) } func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) { if self.isDismissed { return } self.validLayout = (layout, navigationHeight) let headerHeight = navigationHeight + 260.0 transition.updateFrame(node: self.headerBackgroundNode, frame: CGRect(origin: CGPoint(x: -1.0, y: 0), size: CGSize(width: layout.size.width + 2.0, height: headerHeight))) transition.updateFrame(node: self.headerCornerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: headerHeight), size: CGSize(width: layout.size.width, height: 10.0))) } func animateOut(completion: @escaping () -> Void) { guard let (layout, navigationHeight) = self.validLayout else { completion() return } self.isDismissed = true let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring) let headerHeight = navigationHeight + 260.0 transition.updateFrame(node: self.headerBackgroundNode, frame: CGRect(origin: CGPoint(x: -1.0, y: -headerHeight - 10.0), size: CGSize(width: layout.size.width + 2.0, height: headerHeight)), completion: { _ in completion() }) transition.updateFrame(node: self.headerCornerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -10.0), size: CGSize(width: layout.size.width, height: 10.0))) } } public final class WalletApplicationSplashScreen: ViewController { private let theme: WalletTheme public init(theme: WalletTheme) { self.theme = theme let navigationBarTheme = NavigationBarTheme(buttonColor: .white, disabledButtonColor: .white, primaryTextColor: .white, backgroundColor: .clear, separatorColor: .clear, badgeBackgroundColor: theme.navigationBar.badgeBackgroundColor, badgeStrokeColor: theme.navigationBar.badgeStrokeColor, badgeTextColor: theme.navigationBar.badgeTextColor) super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: navigationBarTheme, strings: NavigationBarStrings(back: "", close: ""))) self.statusBar.statusBarStyle = .White } required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override public func loadDisplayNode() { self.displayNode = WalletApplicationSplashScreenNode(theme: self.theme) } override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) (self.displayNode as! WalletApplicationSplashScreenNode).containerLayoutUpdated(layout: layout, navigationHeight: self.navigationHeight, transition: transition) } public func animateOut(completion: @escaping () -> Void) { self.statusBar.statusBarStyle = .Black (self.displayNode as! WalletApplicationSplashScreenNode).animateOut(completion: completion) } }