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 OverlayStatusController import ItemListUI import TelegramStringFormatting private final class WalletTransactionInfoControllerArguments { let copyWalletAddress: () -> Void let sendGrams: () -> Void let displayContextMenu: (WalletTransactionInfoEntryTag, String) -> Void init(copyWalletAddress: @escaping () -> Void, sendGrams: @escaping () -> Void, displayContextMenu: @escaping (WalletTransactionInfoEntryTag, String) -> Void) { self.copyWalletAddress = copyWalletAddress self.sendGrams = sendGrams self.displayContextMenu = displayContextMenu } } private enum WalletTransactionInfoSection: Int32 { case amount case info case comment } private enum WalletTransactionInfoEntryTag: ItemListItemTag { case address case comment func isEqual(to other: ItemListItemTag) -> Bool { if let other = other as? WalletTransactionInfoEntryTag { return self == other } else { return false } } } private enum WalletTransactionInfoEntry: ItemListNodeEntry { case amount(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, WalletTransaction) case infoHeader(PresentationTheme, String) case infoAddress(PresentationTheme, String, String?) case infoCopyAddress(PresentationTheme, String) case infoSendGrams(PresentationTheme, String) case commentHeader(PresentationTheme, String) case comment(PresentationTheme, String) var section: ItemListSectionId { switch self { case .amount: return WalletTransactionInfoSection.amount.rawValue case .infoHeader, .infoAddress, .infoCopyAddress, .infoSendGrams: return WalletTransactionInfoSection.info.rawValue case .commentHeader, .comment: return WalletTransactionInfoSection.comment.rawValue } } var stableId: Int32 { switch self { case .amount: return 0 case .infoHeader: return 1 case .infoAddress: return 2 case .infoCopyAddress: return 3 case .infoSendGrams: return 4 case .commentHeader: return 5 case .comment: return 6 } } static func <(lhs: WalletTransactionInfoEntry, rhs: WalletTransactionInfoEntry) -> Bool { return lhs.stableId < rhs.stableId } func item(_ arguments: WalletTransactionInfoControllerArguments) -> ListViewItem { switch self { case let .amount(theme, strings, dateTimeFormat, walletTransaction): return WalletTransactionHeaderItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, walletTransaction: walletTransaction, sectionId: self.section) case let .infoHeader(theme, text): return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) case let .infoAddress(theme, text, address): return ItemListMultilineTextItem(theme: theme, text: text, enabledEntityTypes: [], font: .monospace, sectionId: self.section, style: .blocks, longTapAction: address == nil ? nil : { if let address = address { arguments.displayContextMenu(WalletTransactionInfoEntryTag.address, address) } }, tag: WalletTransactionInfoEntryTag.address) case let .infoCopyAddress(theme, text): return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.copyWalletAddress() }) case let .infoSendGrams(theme, text): return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.sendGrams() }) case let .commentHeader(theme, text): return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) case let .comment(theme, text): return ItemListMultilineTextItem(theme: theme, text: text, enabledEntityTypes: [], sectionId: self.section, style: .blocks, longTapAction: { arguments.displayContextMenu(WalletTransactionInfoEntryTag.comment, text) }, tag: WalletTransactionInfoEntryTag.comment) } } } private struct WalletTransactionInfoControllerState: Equatable { } private enum WalletTransactionAddress { case list([String]) case none case unknown } private func stringForAddress(strings: PresentationStrings, address: WalletTransactionAddress) -> String { switch address { case let .list(addresses): return addresses.map { formatAddress($0) }.joined(separator: "\n\n") case .none: return strings.Wallet_TransactionInfo_NoAddress case .unknown: return "" } } private func extractAddress(_ walletTransaction: WalletTransaction) -> WalletTransactionAddress { let transferredValue = walletTransaction.transferredValue if transferredValue <= 0 { if walletTransaction.outMessages.isEmpty { return .none } else { var addresses: [String] = [] for message in walletTransaction.outMessages { addresses.append(message.destination) } return .list(addresses) } } else { if let inMessage = walletTransaction.inMessage { return .list([inMessage.source]) } else { return .unknown } } return .none } private func extractDescription(_ walletTransaction: WalletTransaction) -> String { let transferredValue = walletTransaction.transferredValue var text = "" if transferredValue <= 0 { for message in walletTransaction.outMessages { if !text.isEmpty { text.append("\n\n") } text.append(message.textMessage) } } else { if let inMessage = walletTransaction.inMessage { text = inMessage.textMessage } } return text } private func walletTransactionInfoControllerEntries(presentationData: PresentationData, walletTransaction: WalletTransaction, state: WalletTransactionInfoControllerState, walletInfo: WalletInfo?) -> [WalletTransactionInfoEntry] { var entries: [WalletTransactionInfoEntry] = [] entries.append(.amount(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, walletTransaction)) let transferredValue = walletTransaction.transferredValue let address = extractAddress(walletTransaction) var singleAddress: String? let text = stringForAddress(strings: presentationData.strings, address: address) let description = extractDescription(walletTransaction) if transferredValue <= 0 { entries.append(.infoHeader(presentationData.theme, presentationData.strings.Wallet_TransactionInfo_RecipientHeader)) } else { entries.append(.infoHeader(presentationData.theme, presentationData.strings.Wallet_TransactionInfo_SenderHeader)) } var singleAddres: String? if case let .list(list) = address, list.count == 1 { singleAddres = list.first } entries.append(.infoAddress(presentationData.theme, text, singleAddres)) if case .list = address, walletInfo != nil { entries.append(.infoCopyAddress(presentationData.theme, presentationData.strings.Wallet_TransactionInfo_CopyAddress)) entries.append(.infoSendGrams(presentationData.theme, presentationData.strings.Wallet_TransactionInfo_SendGrams)) } if !description.isEmpty { entries.append(.commentHeader(presentationData.theme, presentationData.strings.Wallet_TransactionInfo_CommentHeader)) entries.append(.comment(presentationData.theme, description)) } return entries } func walletTransactionInfoController(context: AccountContext, tonContext: TonContext, walletInfo: WalletInfo?, walletTransaction: WalletTransaction, enableDebugActions: Bool) -> ViewController { let statePromise = ValuePromise(WalletTransactionInfoControllerState(), ignoreRepeated: true) let stateValue = Atomic(value: WalletTransactionInfoControllerState()) let updateState: ((WalletTransactionInfoControllerState) -> WalletTransactionInfoControllerState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } var dismissImpl: (() -> Void)? var presentControllerImpl: ((ViewController, Any?) -> Void)? var pushImpl: ((ViewController) -> Void)? var displayContextMenuImpl: ((WalletTransactionInfoEntryTag, String) -> Void)? let arguments = WalletTransactionInfoControllerArguments(copyWalletAddress: { let address = extractAddress(walletTransaction) if case let .list(addresses) = address, let address = addresses.first { UIPasteboard.general.string = address let presentationData = context.sharedContext.currentPresentationData.with { $0 } presentControllerImpl?(OverlayStatusController(theme: presentationData.theme, strings: presentationData.strings, type: .genericSuccess(presentationData.strings.Wallet_TransactionInfo_AddressCopied, false)), nil) } }, sendGrams: { guard let walletInfo = walletInfo else { return } let address = extractAddress(walletTransaction) if case let .list(addresses) = address, let address = addresses.first { dismissImpl?() pushImpl?(walletSendScreen(context: context, tonContext: tonContext, randomId: arc4random64(), walletInfo: walletInfo, address: address)) } }, displayContextMenu: { tag, text in displayContextMenuImpl?(tag, text) }) let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, statePromise.get()) |> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, WalletTransactionInfoEntry.ItemGenerationArguments)) in let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.Wallet_TransactionInfo_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) let listState = ItemListNodeState(entries: walletTransactionInfoControllerEntries(presentationData: presentationData, walletTransaction: walletTransaction, state: state, walletInfo: walletInfo), style: .blocks, animateChanges: false) return (controllerState, (listState, arguments)) } |> afterDisposed { } let controller = ItemListController(context: context, state: signal) controller.navigationPresentation = .modal controller.enableInteractiveDismiss = true dismissImpl = { [weak controller] in controller?.view.endEditing(true) controller?.dismiss() } presentControllerImpl = { [weak controller] c, a in controller?.present(c, in: .window(.root), with: a) } pushImpl = { [weak controller] c in controller?.push(c) } displayContextMenuImpl = { [weak controller] tag, value in if let strongController = controller { let presentationData = context.sharedContext.currentPresentationData.with { $0 } var resultItemNode: ListViewItemNode? let _ = strongController.frameForItemNode({ itemNode in if let itemNode = itemNode as? ItemListMultilineTextItemNode { if let itemTag = itemNode.tag as? WalletTransactionInfoEntryTag { if itemTag == tag { resultItemNode = itemNode return true } } } return false }) if let resultItemNode = resultItemNode { var actions: [ContextMenuAction] = [] actions.append(ContextMenuAction(content: .text(title: presentationData.strings.Conversation_ContextMenuCopy, accessibilityLabel: presentationData.strings.Conversation_ContextMenuCopy), action: { UIPasteboard.general.string = value })) if enableDebugActions { if case .address = tag { actions.append(ContextMenuAction(content: .text(title: "View Transactions", accessibilityLabel: "View Transactions"), action: { pushImpl?(WalletInfoScreen(context: context, tonContext: tonContext, walletInfo: nil, address: value, enableDebugActions: enableDebugActions)) //dismissImpl?() })) } } let contextMenuController = ContextMenuController(actions: actions) strongController.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak resultItemNode] in if let strongController = controller, let resultItemNode = resultItemNode { return (resultItemNode, resultItemNode.contentBounds.insetBy(dx: 0.0, dy: -2.0), strongController.displayNode, strongController.view.bounds) } else { return nil } })) } } } return controller } class WalletTransactionHeaderItem: ListViewItem, ItemListItem { let theme: PresentationTheme let strings: PresentationStrings let dateTimeFormat: PresentationDateTimeFormat let walletTransaction: WalletTransaction let sectionId: ItemListSectionId let isAlwaysPlain: Bool = true init(theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, walletTransaction: WalletTransaction, sectionId: ItemListSectionId) { self.theme = theme self.strings = strings self.dateTimeFormat = dateTimeFormat self.walletTransaction = walletTransaction self.sectionId = sectionId } func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { async { let node = WalletTransactionHeaderItemNode() let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) node.contentSize = layout.contentSize node.insets = layout.insets Queue.mainQueue().async { completion(node, { return (nil, { _ in apply() }) }) } } } func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { Queue.mainQueue().async { guard let nodeValue = node() as? WalletTransactionHeaderItemNode else { assertionFailure() return } let makeLayout = nodeValue.asyncLayout() async { let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) Queue.mainQueue().async { completion(layout, { _ in apply() }) } } } } } private let titleFont = Font.regular(14.0) private let titleBoldFont = Font.semibold(14.0) private class WalletTransactionHeaderItemNode: ListViewItemNode { private let titleSignNode: TextNode private let titleNode: TextNode private let subtitleNode: TextNode private let iconNode: ASImageNode private let activateArea: AccessibilityAreaNode private var item: WalletTransactionHeaderItem? init() { self.titleSignNode = TextNode() self.titleSignNode.isUserInteractionEnabled = false self.titleSignNode.contentMode = .left self.titleSignNode.contentsScale = UIScreen.main.scale self.titleNode = TextNode() self.titleNode.isUserInteractionEnabled = false self.titleNode.contentMode = .left self.titleNode.contentsScale = UIScreen.main.scale self.subtitleNode = TextNode() self.subtitleNode.isUserInteractionEnabled = false self.subtitleNode.contentMode = .left self.subtitleNode.contentsScale = UIScreen.main.scale self.iconNode = ASImageNode() self.iconNode.displaysAsynchronously = false self.iconNode.displayWithoutProcessing = true self.iconNode.image = UIImage(bundleImageName: "Wallet/BalanceGem")?.precomposed() self.activateArea = AccessibilityAreaNode() self.activateArea.accessibilityTraits = .staticText super.init(layerBacked: false, dynamicBounce: false) self.addSubnode(self.titleSignNode) self.addSubnode(self.titleNode) self.addSubnode(self.subtitleNode) self.addSubnode(self.iconNode) self.addSubnode(self.activateArea) } func asyncLayout() -> (_ item: WalletTransactionHeaderItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { let makeTitleSignLayout = TextNode.asyncLayout(self.titleSignNode) let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode) let iconSize = self.iconNode.image?.size ?? CGSize(width: 10.0, height: 10.0) return { item, params, neighbors in let leftInset: CGFloat = 15.0 + params.leftInset let verticalInset: CGFloat = 24.0 let signString: String let balanceString: String let titleColor: UIColor let transferredValue = item.walletTransaction.transferredValue if transferredValue <= 0 { signString = "-" balanceString = "\(formatBalanceText(-transferredValue, decimalSeparator: item.dateTimeFormat.decimalSeparator))" titleColor = item.theme.list.itemPrimaryTextColor } else { signString = "+" balanceString = "\(formatBalanceText(transferredValue, decimalSeparator: item.dateTimeFormat.decimalSeparator))" titleColor = item.theme.chatList.secretTitleColor } let title = NSMutableAttributedString() if let range = balanceString.range(of: item.dateTimeFormat.decimalSeparator) { let integralPart = String(balanceString[..