mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
646 lines
30 KiB
Swift
646 lines
30 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import AppBundle
|
|
import AccountContext
|
|
import TelegramPresentationData
|
|
import AsyncDisplayKit
|
|
import Display
|
|
import Postbox
|
|
import TelegramCore
|
|
import SolidRoundedButtonNode
|
|
import AnimationUI
|
|
import SwiftSignalKit
|
|
import MergeLists
|
|
|
|
public final class WalletInfoScreen: ViewController {
|
|
private let context: AccountContext
|
|
private let tonContext: TonContext
|
|
private let walletInfo: WalletInfo
|
|
private let address: String
|
|
|
|
private var presentationData: PresentationData
|
|
|
|
public init(context: AccountContext, tonContext: TonContext, walletInfo: WalletInfo, address: String) {
|
|
self.context = context
|
|
self.tonContext = tonContext
|
|
self.walletInfo = walletInfo
|
|
self.address = address
|
|
|
|
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
|
|
|
let defaultNavigationPresentationData = NavigationBarPresentationData(presentationTheme: self.presentationData.theme, presentationStrings: self.presentationData.strings)
|
|
let navigationBarTheme = NavigationBarTheme(buttonColor: .white, disabledButtonColor: .white, primaryTextColor: .white, backgroundColor: .clear, separatorColor: .clear, badgeBackgroundColor: defaultNavigationPresentationData.theme.badgeBackgroundColor, badgeStrokeColor: defaultNavigationPresentationData.theme.badgeStrokeColor, badgeTextColor: defaultNavigationPresentationData.theme.badgeTextColor)
|
|
|
|
super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: navigationBarTheme, strings: defaultNavigationPresentationData.strings))
|
|
|
|
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.Common_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.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, tonContext: self.tonContext, walletInfo: self.walletInfo))
|
|
}
|
|
|
|
override public func loadDisplayNode() {
|
|
self.displayNode = WalletInfoScreenNode(account: self.context.account, tonContext: self.tonContext, presentationData: self.presentationData, walletInfo: self.walletInfo, address: self.address, sendAction: { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.push(walletSendScreen(context: strongSelf.context, tonContext: strongSelf.tonContext, walletInfo: strongSelf.walletInfo))
|
|
}, receiveAction: { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.push(walletReceiveScreen(context: strongSelf.context, tonContext: strongSelf.tonContext, walletInfo: strongSelf.walletInfo, address: strongSelf.address))
|
|
}, openTransaction: { [weak self] transaction in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.push(walletTransactionInfoController(context: strongSelf.context, walletTransaction: transaction))
|
|
})
|
|
|
|
self.displayNodeDidLoad()
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
private final class WalletInfoBalanceNode: ASDisplayNode {
|
|
private let balanceTextNode: ImmediateTextNode
|
|
private let balanceIconNode: ASImageNode
|
|
|
|
var balance: String = " " {
|
|
didSet {
|
|
self.balanceTextNode.attributedText = NSAttributedString(string: self.balance, font: Font.bold(39.0), textColor: .white)
|
|
}
|
|
}
|
|
|
|
init(theme: PresentationTheme) {
|
|
self.balanceTextNode = ImmediateTextNode()
|
|
self.balanceTextNode.displaysAsynchronously = false
|
|
self.balanceTextNode.attributedText = NSAttributedString(string: " ", font: Font.bold(39.0), textColor: .white)
|
|
self.balanceTextNode.layer.minificationFilter = .linear
|
|
|
|
self.balanceIconNode = ASImageNode()
|
|
self.balanceIconNode.displaysAsynchronously = false
|
|
self.balanceIconNode.displayWithoutProcessing = true
|
|
self.balanceIconNode.image = UIImage(bundleImageName: "Wallet/BalanceGem")?.precomposed()
|
|
|
|
super.init()
|
|
|
|
self.addSubnode(self.balanceTextNode)
|
|
self.addSubnode(self.balanceIconNode)
|
|
}
|
|
|
|
func update(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
|
|
let sideInset: CGFloat = 16.0
|
|
let balanceIconSpacing: CGFloat = 8.0
|
|
|
|
let balanceTextSize = self.balanceTextNode.updateLayout(CGSize(width: width - sideInset * 2.0, height: 200.0))
|
|
let balanceIconSize = self.balanceIconNode.image?.size ?? CGSize(width: 38.0, height: 34.0)
|
|
|
|
let balanceOrigin = CGPoint(x: floor((width - balanceTextSize.width - balanceIconSpacing - balanceIconSize.width / 2.0) / 2.0), y: 0.0)
|
|
|
|
let balanceTextFrame = CGRect(origin: balanceOrigin, size: balanceTextSize)
|
|
let balanceIconFrame = CGRect(origin: CGPoint(x: balanceTextFrame.maxX + balanceIconSpacing, y: balanceTextFrame.minY + floor((balanceTextFrame.height - balanceIconSize.height) / 2.0)), size: balanceIconSize)
|
|
transition.updateFrameAdditive(node: self.balanceTextNode, frame: balanceTextFrame)
|
|
transition.updateFrameAdditive(node: self.balanceIconNode, frame: balanceIconFrame)
|
|
|
|
return balanceTextSize.height
|
|
}
|
|
}
|
|
|
|
private final class WalletInfoHeaderNode: ASDisplayNode {
|
|
var balance: Int64?
|
|
|
|
let balanceNode: WalletInfoBalanceNode
|
|
private let balanceSubtitleNode: ImmediateTextNode
|
|
private let receiveButtonNode: SolidRoundedButtonNode
|
|
private let sendButtonNode: SolidRoundedButtonNode
|
|
private let headerBackgroundNode: ASImageNode
|
|
|
|
init(theme: PresentationTheme, sendAction: @escaping () -> Void, receiveAction: @escaping () -> Void) {
|
|
self.balanceNode = WalletInfoBalanceNode(theme: theme)
|
|
|
|
self.balanceSubtitleNode = ImmediateTextNode()
|
|
self.balanceSubtitleNode.displaysAsynchronously = false
|
|
self.balanceSubtitleNode.attributedText = NSAttributedString(string: "your balance", font: Font.regular(13), textColor: UIColor(white: 1.0, alpha: 0.6))
|
|
|
|
self.headerBackgroundNode = ASImageNode()
|
|
self.headerBackgroundNode.displaysAsynchronously = false
|
|
self.headerBackgroundNode.displayWithoutProcessing = true
|
|
self.headerBackgroundNode.image = generateImage(CGSize(width: 20.0, height: 20.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: size.height / 2.0), size: size))
|
|
})?.stretchableImage(withLeftCapWidth: 10, topCapHeight: 1)
|
|
|
|
self.receiveButtonNode = SolidRoundedButtonNode(title: "Receive", icon: UIImage(bundleImageName: "Wallet/ReceiveButtonIcon"), theme: SolidRoundedButtonTheme(backgroundColor: .white, foregroundColor: .black), height: 50.0, cornerRadius: 10.0, gloss: false)
|
|
self.sendButtonNode = SolidRoundedButtonNode(title: "Send", icon: UIImage(bundleImageName: "Wallet/SendButtonIcon"), theme: SolidRoundedButtonTheme(backgroundColor: .white, foregroundColor: .black), height: 50.0, cornerRadius: 10.0, gloss: false)
|
|
|
|
super.init()
|
|
|
|
self.addSubnode(self.headerBackgroundNode)
|
|
self.addSubnode(self.receiveButtonNode)
|
|
self.addSubnode(self.sendButtonNode)
|
|
self.addSubnode(self.balanceNode)
|
|
self.addSubnode(self.balanceSubtitleNode)
|
|
|
|
self.receiveButtonNode.pressed = {
|
|
receiveAction()
|
|
}
|
|
self.sendButtonNode.pressed = {
|
|
sendAction()
|
|
}
|
|
}
|
|
|
|
func update(size: CGSize, navigationHeight: CGFloat, offset: CGFloat, transition: ContainedViewLayoutTransition) {
|
|
let sideInset: CGFloat = 16.0
|
|
let buttonSideInset: CGFloat = 48.0
|
|
let titleSpacing: CGFloat = 10.0
|
|
let termsSpacing: CGFloat = 10.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 - sideInset
|
|
let maxButtonsOffset = maxOffset
|
|
let buttonTransition: CGFloat = max(0.0, min(1.0, (effectiveOffset - minButtonsOffset) / (maxButtonsOffset - minButtonsOffset)))
|
|
let buttonAlpha = buttonTransition * 1.0
|
|
|
|
let balanceSubtitleSize = self.balanceSubtitleNode.updateLayout(CGSize(width: size.width - sideInset * 2.0, height: 200.0))
|
|
|
|
let balanceHeight = self.balanceNode.update(width: size.width, transition: transition)
|
|
let balanceSize = CGSize(width: size.width, height: balanceHeight)
|
|
|
|
let minHeaderScale: CGFloat = 0.435
|
|
|
|
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 headerScaleTransition: CGFloat = max(0.0, min(1.0, (effectiveOffset - minHeaderOffset) / (maxHeaderOffset - minHeaderOffset)))
|
|
let headerPositionTransition: CGFloat = max(0.0, (effectiveOffset - minHeaderOffset) / (maxOffset - minHeaderOffset))
|
|
let headerY = headerPositionTransition * maxHeaderY + (1.0 - headerPositionTransition) * minHeaderY
|
|
let headerScale = headerScaleTransition * 1.0 + (1.0 - headerScaleTransition) * minHeaderScale
|
|
|
|
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)
|
|
|
|
transition.updateFrameAdditive(node: self.balanceSubtitleNode, frame: CGRect(origin: CGPoint(x: floor((size.width - balanceSubtitleSize.width) / 2.0), y: balanceFrame.midY + (balanceFrame.height / 2.0 * headerScale) + balanceSubtitleSpacing), size: balanceSubtitleSize))
|
|
|
|
let headerHeight: CGFloat = 1000.0
|
|
transition.updateFrame(node: self.headerBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: effectiveOffset + 10.0 - headerHeight), size: CGSize(width: size.width, height: headerHeight)))
|
|
|
|
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))
|
|
|
|
var receiveButtonFrame: CGRect
|
|
if let balance = self.balance, balance > 0 {
|
|
receiveButtonFrame = leftButtonFrame
|
|
self.receiveButtonNode.isHidden = false
|
|
self.sendButtonNode.isHidden = false
|
|
} else {
|
|
receiveButtonFrame = fullButtonFrame
|
|
if self.balance == nil {
|
|
self.receiveButtonNode.isHidden = true
|
|
self.sendButtonNode.isHidden = true
|
|
} else {
|
|
self.receiveButtonNode.isHidden = false
|
|
self.sendButtonNode.isHidden = true
|
|
}
|
|
}
|
|
if self.balance == nil {
|
|
self.balanceNode.isHidden = true
|
|
self.balanceSubtitleNode.isHidden = true
|
|
} else {
|
|
self.balanceNode.isHidden = false
|
|
self.balanceSubtitleNode.isHidden = false
|
|
}
|
|
transition.updateFrame(node: self.receiveButtonNode, frame: receiveButtonFrame)
|
|
transition.updateAlpha(node: self.receiveButtonNode, alpha: buttonAlpha)
|
|
self.receiveButtonNode.updateLayout(width: receiveButtonFrame.width, transition: transition)
|
|
transition.updateFrame(node: self.sendButtonNode, frame: sendButtonFrame)
|
|
transition.updateAlpha(node: self.sendButtonNode, alpha: buttonAlpha)
|
|
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
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func animateIn() {
|
|
self.sendButtonNode.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)
|
|
}
|
|
}
|
|
|
|
private struct WalletInfoListTransaction {
|
|
let deletions: [ListViewDeleteItem]
|
|
let insertions: [ListViewInsertItem]
|
|
let updates: [ListViewUpdateItem]
|
|
}
|
|
|
|
private enum WalletInfoListEntryId: Hashable {
|
|
case empty
|
|
case transaction(WalletTransactionId)
|
|
}
|
|
|
|
private enum WalletInfoListEntry: Equatable, Comparable, Identifiable {
|
|
case empty(String)
|
|
case transaction(Int, WalletTransaction)
|
|
|
|
var stableId: WalletInfoListEntryId {
|
|
switch self {
|
|
case .empty:
|
|
return .empty
|
|
case let .transaction(_, transaction):
|
|
return .transaction(transaction.transactionId)
|
|
}
|
|
}
|
|
|
|
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: PresentationTheme, strings: PresentationStrings, action: @escaping (WalletTransaction) -> Void) -> ListViewItem {
|
|
switch self {
|
|
case let .empty(address):
|
|
return WalletInfoEmptyItem(theme: theme, strings: strings, address: address)
|
|
case let .transaction(_, transaction):
|
|
return WalletInfoTransactionItem(theme: theme, strings: strings, walletTransaction: transaction, action: {
|
|
action(transaction)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
private func preparedTransition(from fromEntries: [WalletInfoListEntry], to toEntries: [WalletInfoListEntry], presentationData: PresentationData, action: @escaping (WalletTransaction) -> 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, action: action), directionHint: nil) }
|
|
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(theme: presentationData.theme, strings: presentationData.strings, action: action), directionHint: nil) }
|
|
|
|
return WalletInfoListTransaction(deletions: deletions, insertions: insertions, updates: updates)
|
|
}
|
|
|
|
private final class WalletInfoScreenNode: ViewControllerTracingNode {
|
|
private let account: Account
|
|
private let tonContext: TonContext
|
|
private var presentationData: PresentationData
|
|
private let walletInfo: WalletInfo
|
|
private let address: String
|
|
private let openTransaction: (WalletTransaction) -> Void
|
|
|
|
private let headerNode: WalletInfoHeaderNode
|
|
private let listNode: ListView
|
|
|
|
private var enqueuedTransactions: [WalletInfoListTransaction] = []
|
|
|
|
private var validLayout: (ContainerViewLayout, CGFloat)?
|
|
|
|
private let balanceDisposable = MetaDisposable()
|
|
private let transactionListDisposable = MetaDisposable()
|
|
|
|
private var listOffset: CGFloat?
|
|
private var loadingMoreTransactions: Bool = false
|
|
private var canLoadMoreTransactions: Bool = true
|
|
|
|
private var currentEntries: [WalletInfoListEntry]?
|
|
|
|
init(account: Account, tonContext: TonContext, presentationData: PresentationData, walletInfo: WalletInfo, address: String, sendAction: @escaping () -> Void, receiveAction: @escaping () -> Void, openTransaction: @escaping (WalletTransaction) -> Void) {
|
|
self.account = account
|
|
self.tonContext = tonContext
|
|
self.presentationData = presentationData
|
|
self.walletInfo = walletInfo
|
|
self.address = address
|
|
self.openTransaction = openTransaction
|
|
|
|
self.headerNode = WalletInfoHeaderNode(theme: presentationData.theme, sendAction: sendAction, receiveAction: receiveAction)
|
|
|
|
self.listNode = ListView()
|
|
self.listNode.verticalScrollIndicatorColor = self.presentationData.theme.list.scrollIndicatorColor
|
|
self.listNode.verticalScrollIndicatorFollowsOverscroll = true
|
|
|
|
super.init()
|
|
|
|
self.backgroundColor = .white
|
|
|
|
self.balanceDisposable.set((currentWalletBalance(publicKey: walletInfo.publicKey, tonInstance: tonContext.instance)
|
|
|> deliverOnMainQueue).start(next: { [weak self] value in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
let firstTime = strongSelf.headerNode.balance == nil
|
|
strongSelf.headerNode.balanceNode.balance = formatBalanceText(max(0, value.rawValue))
|
|
strongSelf.headerNode.balance = max(0, value.rawValue)
|
|
if let (layout, navigationHeight) = strongSelf.validLayout {
|
|
strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate)
|
|
}
|
|
if firstTime {
|
|
strongSelf.headerNode.animateIn()
|
|
}
|
|
}))
|
|
|
|
self.addSubnode(self.listNode)
|
|
self.addSubnode(self.headerNode)
|
|
|
|
self.listNode.updateFloatingHeaderOffset = { [weak self] offset, listTransition in
|
|
guard let strongSelf = self, let (layout, navigationHeight) = strongSelf.validLayout else {
|
|
return
|
|
}
|
|
|
|
strongSelf.listOffset = offset
|
|
|
|
strongSelf.headerNode.update(size: strongSelf.headerNode.bounds.size, navigationHeight: navigationHeight, offset: offset, transition: listTransition)
|
|
}
|
|
|
|
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.canLoadMoreTransactions {
|
|
strongSelf.loadMoreTransactions()
|
|
}
|
|
}
|
|
|
|
self.listNode.didEndScrolling = { [weak self] in
|
|
guard let strongSelf = self, let (_, navigationHeight) = strongSelf.validLayout else {
|
|
return
|
|
}
|
|
switch strongSelf.listNode.visibleContentOffset() {
|
|
case let .known(offset):
|
|
if offset < strongSelf.listNode.insets.top {
|
|
/*if offset > strongSelf.listNode.insets.top / 2.0 {
|
|
strongSelf.scrollToHideHeader()
|
|
} else {
|
|
strongSelf.scrollToTop()
|
|
}*/
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
self.refreshTransactions()
|
|
}
|
|
|
|
func scrollToHideHeader() {
|
|
guard let (_, navigationHeight) = self.validLayout else {
|
|
return
|
|
}
|
|
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(navigationHeight), animated: true, curve: .Spring(duration: 0.4), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
|
}
|
|
|
|
func scrollToTop() {
|
|
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 headerFrame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: headerHeight))
|
|
transition.updateFrame(node: self.headerNode, frame: headerFrame)
|
|
self.headerNode.update(size: headerFrame.size, navigationHeight: navigationHeight, offset: self.listOffset ?? 0.0, transition: transition)
|
|
|
|
transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
|
|
|
var duration: Double = 0.0
|
|
var curve: UInt = 0
|
|
switch transition {
|
|
case .immediate:
|
|
break
|
|
case let .animated(animationDuration, animationCurve):
|
|
duration = animationDuration
|
|
switch animationCurve {
|
|
case .easeInOut, .custom:
|
|
break
|
|
case .spring:
|
|
curve = 7
|
|
}
|
|
}
|
|
|
|
let listViewCurve: ListViewAnimationCurve
|
|
if curve == 7 {
|
|
listViewCurve = .Spring(duration: duration)
|
|
} else {
|
|
listViewCurve = .Default(duration: duration)
|
|
}
|
|
|
|
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: listViewCurve), 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.transactionListDisposable.set((getWalletTransactions(address: self.address, previousId: nil, tonInstance: self.tonContext.instance)
|
|
|> deliverOnMainQueue).start(next: { [weak self] transactions in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.transactionsLoaded(isReload: true, transactions: transactions)
|
|
}, error: { [weak self] _ in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
}))
|
|
}
|
|
|
|
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):
|
|
lastTransactionId = transaction.transactionId
|
|
case .empty:
|
|
break
|
|
}
|
|
}
|
|
self.transactionListDisposable.set((getWalletTransactions(address: self.address, previousId: lastTransactionId, tonInstance: self.tonContext.instance)
|
|
|> deliverOnMainQueue).start(next: { [weak self] transactions in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.transactionsLoaded(isReload: false, transactions: transactions)
|
|
}, error: { [weak self] _ in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
}))
|
|
}
|
|
|
|
private func transactionsLoaded(isReload: Bool, transactions: [WalletTransaction]) {
|
|
self.loadingMoreTransactions = false
|
|
self.canLoadMoreTransactions = transactions.count > 2
|
|
|
|
let isFirst = self.currentEntries == nil
|
|
|
|
var updatedEntries: [WalletInfoListEntry] = []
|
|
if isReload {
|
|
for transaction in transactions {
|
|
updatedEntries.append(.transaction(updatedEntries.count, transaction))
|
|
}
|
|
if updatedEntries.isEmpty {
|
|
updatedEntries.append(.empty(self.address))
|
|
}
|
|
} else {
|
|
updatedEntries = self.currentEntries ?? []
|
|
updatedEntries = updatedEntries.filter { entry in
|
|
if case .empty = entry {
|
|
return false
|
|
} else {
|
|
return true
|
|
}
|
|
}
|
|
var existingIds = Set<WalletTransactionId>()
|
|
for entry in updatedEntries {
|
|
switch entry {
|
|
case let .transaction(_, transaction):
|
|
existingIds.insert(transaction.transactionId)
|
|
case .empty:
|
|
break
|
|
}
|
|
}
|
|
for transaction in transactions {
|
|
if !existingIds.contains(transaction.transactionId) {
|
|
existingIds.insert(transaction.transactionId)
|
|
updatedEntries.append(.transaction(updatedEntries.count, transaction))
|
|
}
|
|
}
|
|
if updatedEntries.isEmpty {
|
|
updatedEntries.append(.empty(self.address))
|
|
}
|
|
}
|
|
|
|
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)
|
|
})
|
|
self.currentEntries = updatedEntries
|
|
|
|
self.enqueuedTransactions.append(transaction)
|
|
self.dequeueTransaction()
|
|
|
|
if isFirst {
|
|
self.listNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
}
|
|
}
|
|
|
|
private func dequeueTransaction() {
|
|
guard let layout = 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
|
|
})
|
|
}
|
|
}
|
|
|
|
func formatBalanceText(_ value: Int64) -> String {
|
|
var balanceText = "\(abs(value))"
|
|
while balanceText.count < 10 {
|
|
balanceText.insert("0", at: balanceText.startIndex)
|
|
}
|
|
balanceText.insert(".", at: balanceText.index(balanceText.endIndex, offsetBy: -9))
|
|
while true {
|
|
if balanceText.hasSuffix("0") {
|
|
if balanceText.hasSuffix(".0") {
|
|
break
|
|
} else {
|
|
balanceText.removeLast()
|
|
}
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
if value < 0 {
|
|
balanceText.insert("-", at: balanceText.startIndex)
|
|
}
|
|
return balanceText
|
|
}
|