Wallet UI improvements

This commit is contained in:
Ilya Laktyushin 2019-10-12 03:37:26 +03:00
parent 8341aecc01
commit bc22d4bb87
35 changed files with 2541 additions and 2180 deletions

View File

@ -4787,7 +4787,7 @@ Any member of this group will be able to see messages in the channel.";
"Wallet.Info.TransactionTo" = "to";
"Wallet.Info.TransactionFrom" = "from";
"Wallet.Info.Updating" = "updating";
"Wallet.Info.TransactionBlockchainFee" = "%@ blockchain fee";
"Wallet.Info.TransactionBlockchainFee" = "%@ blockchain fees";
"Wallet.Info.TransactionPendingHeader" = "Pending";
"Wallet.Qr.ScanCode" = "Scan QR Code";
"Wallet.Qr.Title" = "QR Code";
@ -4964,3 +4964,5 @@ Any member of this group will be able to see messages in the channel.";
"Wallet.Time.PreciseDate_m12" = "Dec %1$@, %2$@ at %3$@";
"Wallet.VoiceOver.Editing.ClearText" = "Clear text";
"Wallet.Receive.ShareInvoiceUrlInfo" = "Share this link with other Gram wallet owners to receive %@ Grams from them.";

View File

@ -157,7 +157,7 @@ public final class AuthorizationSequenceCountrySelectionController: ViewControll
self.strings = strings
self.displayCodes = displayCodes
super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: NavigationBarTheme(rootControllerTheme: theme), strings: NavigationBarStrings(presentationStrings: strings)))
super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: NavigationBarTheme(rootControllerTheme: theme), strings: NavigationBarStrings(presentationStrings: strings)))
self.navigationPresentation = .modal

View File

@ -324,7 +324,7 @@ open class NavigationController: UINavigationController, ContainableController,
if let existingModalContainer = existingModalContainer {
modalContainer = existingModalContainer
} else {
modalContainer = NavigationModalContainer(theme: self.theme, controllerRemoved: { [weak self] controller in
modalContainer = NavigationModalContainer(theme: self.theme, flat: navigationLayout.modal[i].flat, controllerRemoved: { [weak self] controller in
self?.controllerRemoved(controller)
})
self.modalContainers.append(modalContainer)
@ -427,6 +427,7 @@ open class NavigationController: UINavigationController, ContainableController,
var previousModalContainer: NavigationModalContainer?
var visibleModalCount = 0
var topModalIsFlat = false
var topModalDismissProgress: CGFloat = 0.0
for i in (0 ..< navigationLayout.modal.count).reversed() {
@ -483,6 +484,8 @@ open class NavigationController: UINavigationController, ContainableController,
}
previousModalContainer = modalContainer
}
topModalIsFlat = modalContainer.flat
}
switch navigationLayout.root {
@ -607,7 +610,7 @@ open class NavigationController: UINavigationController, ContainableController,
let effectiveRootModalDismissProgress: CGFloat
let additionalModalFrameProgress: CGFloat
if visibleModalCount == 1 {
effectiveRootModalDismissProgress = topModalDismissProgress
effectiveRootModalDismissProgress = topModalIsFlat ? 1.0 : topModalDismissProgress
additionalModalFrameProgress = 0.0
} else if visibleModalCount == 2 {
effectiveRootModalDismissProgress = 0.0
@ -667,7 +670,10 @@ open class NavigationController: UINavigationController, ContainableController,
}
let maxScale: CGFloat
let maxOffset: CGFloat
if visibleModalCount == 1 {
if topModalIsFlat {
maxScale = 1.0
maxOffset = 0.0
} else if visibleModalCount == 1 {
maxScale = (layout.size.width - 16.0 * 2.0) / layout.size.width
maxOffset = (topInset - (layout.size.height - layout.size.height * maxScale) / 2.0)
} else {

View File

@ -10,6 +10,7 @@ enum RootNavigationLayout {
struct ModalContainerLayout {
var controllers: [ViewController]
var flat: Bool
}
struct NavigationLayout {
@ -23,6 +24,7 @@ func makeNavigationLayout(mode: NavigationControllerMode, layout: ContainerViewL
for controller in controllers {
let requiresModal: Bool
var beginsModal: Bool = false
var flatModal: Bool = false
switch controller.navigationPresentation {
case .default:
requiresModal = false
@ -31,6 +33,10 @@ func makeNavigationLayout(mode: NavigationControllerMode, layout: ContainerViewL
case .modal:
requiresModal = true
beginsModal = true
case .flatModal:
requiresModal = true
beginsModal = true
flatModal = true
case .modalInLargeLayout:
switch layout.metrics.widthClass {
case .compact:
@ -40,14 +46,17 @@ func makeNavigationLayout(mode: NavigationControllerMode, layout: ContainerViewL
}
}
if requiresModal {
controller._presentedInModal = true
if beginsModal || modalStack.isEmpty {
modalStack.append(ModalContainerLayout(controllers: [controller]))
modalStack.append(ModalContainerLayout(controllers: [controller], flat: flatModal))
} else {
modalStack[modalStack.count - 1].controllers.append(controller)
}
} else if !modalStack.isEmpty {
controller._presentedInModal = true
modalStack[modalStack.count - 1].controllers.append(controller)
} else {
controller._presentedInModal = false
rootControllers.append(controller)
}
}

View File

@ -5,6 +5,7 @@ import SwiftSignalKit
final class NavigationModalContainer: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDelegate {
private var theme: NavigationControllerTheme
let flat: Bool
private let dim: ASDisplayNode
private let scrollNode: ASScrollNode
@ -40,8 +41,9 @@ final class NavigationModalContainer: ASDisplayNode, UIScrollViewDelegate, UIGes
}
}
init(theme: NavigationControllerTheme, controllerRemoved: @escaping (ViewController) -> Void) {
init(theme: NavigationControllerTheme, flat: Bool, controllerRemoved: @escaping (ViewController) -> Void) {
self.theme = theme
self.flat = flat
self.dim = ASDisplayNode()
self.dim.alpha = 0.0
@ -316,6 +318,9 @@ final class NavigationModalContainer: ASDisplayNode, UIScrollViewDelegate, UIGes
}
var topInset: CGFloat = 10.0
if self.flat, let preferredSize = controllers.last?.preferredContentSizeForLayout(layout) {
topInset = layout.size.height - preferredSize.height
}
if let statusBarHeight = layout.statusBarHeight {
topInset += statusBarHeight
}

View File

@ -102,7 +102,7 @@ final class NavigationModalFrame: ASDisplayNode {
}
}
private func updateShades(layout: ContainerViewLayout, progress: CGFloat, additionalProgress: CGFloat, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
private func updateShades(layout: ContainerViewLayout, progress: CGFloat, additionalProgress: CGFloat, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
let sideInset: CGFloat = 16.0
var topInset: CGFloat = 0.0
if let statusBarHeight = layout.statusBarHeight {

View File

@ -105,7 +105,7 @@ enum NavigationPreviousAction: Equatable {
open class NavigationBar: ASDisplayNode {
private var presentationData: NavigationBarPresentationData
private var validLayout: (CGSize, CGFloat, CGFloat)?
private var validLayout: (CGSize, CGFloat, CGFloat, CGFloat)?
private var requestedLayout: Bool = false
var requestContainerLayout: (ContainedViewLayoutTransition) -> Void = { _ in }
@ -789,16 +789,16 @@ open class NavigationBar: ASDisplayNode {
if let validLayout = self.validLayout, self.requestedLayout {
self.requestedLayout = false
self.updateLayout(size: validLayout.0, leftInset: validLayout.1, rightInset: validLayout.2, transition: .immediate)
self.updateLayout(size: validLayout.0, defaultHeight: validLayout.1, leftInset: validLayout.2, rightInset: validLayout.3, transition: .immediate)
}
}
func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) {
func updateLayout(size: CGSize, defaultHeight: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) {
if self.layoutSuspended {
return
}
self.validLayout = (size, leftInset, rightInset)
self.validLayout = (size, defaultHeight, leftInset, rightInset)
let leftButtonInset: CGFloat = leftInset + 16.0
let backButtonInset: CGFloat = leftInset + 27.0
@ -809,7 +809,7 @@ open class NavigationBar: ASDisplayNode {
let contentNodeFrame: CGRect
switch contentNode.mode {
case .replacement:
expansionHeight = contentNode.height - 44.0
expansionHeight = contentNode.height - defaultHeight
contentNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))
case .expansion:
expansionHeight = contentNode.height
@ -821,7 +821,7 @@ open class NavigationBar: ASDisplayNode {
transition.updateFrame(node: self.stripeNode, frame: CGRect(x: 0.0, y: size.height, width: size.width, height: UIScreenPixel))
let nominalHeight: CGFloat = self.collapsed ? 32.0 : 44.0
let nominalHeight: CGFloat = self.collapsed ? 32.0 : defaultHeight
let contentVerticalOrigin = size.height - nominalHeight - expansionHeight
var leftTitleInset: CGFloat = leftInset + 1.0
@ -1063,16 +1063,16 @@ open class NavigationBar: ASDisplayNode {
}
}
public var contentHeight: CGFloat {
public func contentHeight(defaultHeight: CGFloat) -> CGFloat {
if let contentNode = self.contentNode {
switch contentNode.mode {
case .expansion:
return 44.0 + contentNode.height
return defaultHeight + contentNode.height
case .replacement:
return contentNode.height
}
} else {
return 44.0
return defaultHeight
}
}

View File

@ -16,6 +16,8 @@ public enum StatusBarStyle {
self = .White
case .blackOpaque:
self = .Black
case .darkContent:
self = .Black
}
}

View File

@ -5,12 +5,15 @@ import SwiftSignalKit
public enum TooltipControllerContent: Equatable {
case text(String)
case attributedText(NSAttributedString)
case iconAndText(UIImage, String)
var text: String {
switch self {
case let .text(text), let .iconAndText(_, text):
return text
case let .attributedText(text):
return text.string
}
}

View File

@ -30,7 +30,11 @@ final class TooltipControllerNode: ASDisplayNode {
self.imageNode.image = content.image
self.textNode = ImmediateTextNode()
self.textNode.attributedText = NSAttributedString(string: content.text, font: Font.regular(14.0), textColor: .white, paragraphAlignment: .center)
if case let .attributedText(text) = content {
self.textNode.attributedText = text
} else {
self.textNode.attributedText = NSAttributedString(string: content.text, font: Font.regular(14.0), textColor: .white, paragraphAlignment: .center)
}
self.textNode.isUserInteractionEnabled = false
self.textNode.displaysAsynchronously = false
self.textNode.maximumNumberOfLines = 0

View File

@ -62,6 +62,7 @@ public enum ViewControllerNavigationPresentation {
case `default`
case master
case modal
case flatModal
case modalInLargeLayout
}
@ -121,6 +122,7 @@ public enum ViewControllerNavigationPresentation {
}
open var navigationPresentation: ViewControllerNavigationPresentation = .default
var _presentedInModal: Bool = false
public var presentationArguments: Any?
@ -319,7 +321,14 @@ public enum ViewControllerNavigationPresentation {
private func updateNavigationBarLayout(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
let statusBarHeight: CGFloat = layout.statusBarHeight ?? 0.0
let navigationBarHeight: CGFloat = statusBarHeight + (self.navigationBar?.contentHeight ?? 44.0)
let defaultNavigationBarHeight: CGFloat
if self._presentedInModal {
defaultNavigationBarHeight = 56.0
} else {
defaultNavigationBarHeight = 44.0
}
let navigationBarHeight: CGFloat = statusBarHeight + (self.navigationBar?.contentHeight(defaultHeight: defaultNavigationBarHeight) ?? defaultNavigationBarHeight)
let navigationBarOffset: CGFloat
if statusBarHeight.isZero {
navigationBarOffset = 0.0
@ -342,7 +351,7 @@ public enum ViewControllerNavigationPresentation {
if let contentNode = navigationBar.contentNode, case .expansion = contentNode.mode, !self.displayNavigationBar {
navigationBarFrame.origin.y += contentNode.height + statusBarHeight
}
navigationBar.updateLayout(size: navigationBarFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: transition)
navigationBar.updateLayout(size: navigationBarFrame.size, defaultHeight: defaultNavigationBarHeight, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: transition)
if !transition.isAnimated {
navigationBar.layer.cancelAnimationsRecursive(key: "bounds")
navigationBar.layer.cancelAnimationsRecursive(key: "position")

View File

@ -848,7 +848,7 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode {
switch self.contentNodes {
case let .standard(node):
if let handleNodeContainer = node.handleNodeContainer, handleNodeContainer.isUserInteractionEnabled, handleNodeContainer.frame.insetBy(dx: 0.0, dy: -5.0).contains(point) {
if let handleNode = node.handleNode, handleNode.convert(handleNode.bounds, to: self).insetBy(dx: -5.0, dy: -5.0).contains(point) {
if let handleNode = node.handleNode, handleNode.convert(handleNode.bounds, to: self).insetBy(dx: -16.0, dy: -16.0).contains(point) {
return handleNodeContainer.view
} else {
return nil

View File

@ -78,9 +78,17 @@ public final class SolidRoundedButtonNode: ASDisplayNode {
if highlighted {
strongSelf.buttonBackgroundNode.layer.removeAnimation(forKey: "opacity")
strongSelf.buttonBackgroundNode.alpha = 0.55
strongSelf.labelNode.layer.removeAnimation(forKey: "opacity")
strongSelf.labelNode.alpha = 0.55
strongSelf.iconNode.layer.removeAnimation(forKey: "opacity")
strongSelf.iconNode.alpha = 0.55
} else {
strongSelf.buttonBackgroundNode.alpha = 1.0
strongSelf.buttonBackgroundNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2)
strongSelf.labelNode.alpha = 1.0
strongSelf.labelNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2)
strongSelf.iconNode.alpha = 1.0
strongSelf.iconNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2)
}
}
}

View File

@ -162,7 +162,7 @@ private final class NetworkTypeManagerImpl {
var currentCellularType: CellularNetworkType
var cellularTypeObserver: NSObjectProtocol?
#endif
init(queue: Queue, updated: @escaping (NetworkType) -> Void) {
self.queue = queue
self.updated = updated

View File

@ -3,28 +3,60 @@ import UIKit
import Display
import AppBundle
public struct PresentationResourcesSettings {
public static let editProfile = UIImage(bundleImageName: "Settings/MenuIcons/EditProfile")?.precomposed()
public static let proxy = UIImage(bundleImageName: "Settings/MenuIcons/Proxy")?.precomposed()
public static let savedMessages = UIImage(bundleImageName: "Settings/MenuIcons/SavedMessages")?.precomposed()
public static let recentCalls = UIImage(bundleImageName: "Settings/MenuIcons/RecentCalls")?.precomposed()
public static let stickers = UIImage(bundleImageName: "Settings/MenuIcons/Stickers")?.precomposed()
public static let notifications = UIImage(bundleImageName: "Settings/MenuIcons/Notifications")?.precomposed()
public static let security = UIImage(bundleImageName: "Settings/MenuIcons/Security")?.precomposed()
public static let dataAndStorage = UIImage(bundleImageName: "Settings/MenuIcons/DataAndStorage")?.precomposed()
public static let appearance = UIImage(bundleImageName: "Settings/MenuIcons/Appearance")?.precomposed()
public static let language = UIImage(bundleImageName: "Settings/MenuIcons/Language")?.precomposed()
public static let wallet = generateTintedImage(image: UIImage(bundleImageName: "Settings/MenuIcons/Wallet"), color: UIColor(rgb: 0x1b1b1c))
public static let passport = UIImage(bundleImageName: "Settings/MenuIcons/Passport")?.precomposed()
public static let watch = UIImage(bundleImageName: "Settings/MenuIcons/Watch")?.precomposed()
public static let support = UIImage(bundleImageName: "Settings/MenuIcons/Support")?.precomposed()
public static let faq = UIImage(bundleImageName: "Settings/MenuIcons/Faq")?.precomposed()
public static let addAccount = UIImage(bundleImageName: "Settings/MenuIcons/AddAccount")?.precomposed()
public static let setPasscode = UIImage(bundleImageName: "Settings/MenuIcons/SetPasscode")?.precomposed()
public static let clearCache = UIImage(bundleImageName: "Settings/MenuIcons/ClearCache")?.precomposed()
public static let changePhoneNumber = UIImage(bundleImageName: "Settings/MenuIcons/ChangePhoneNumber")?.precomposed()
private func drawBorder(context: CGContext, rect: CGRect) {
context.setLineWidth(UIScreenPixel)
context.setStrokeColor(UIColor(rgb: 0xffffff, alpha: 0.25).cgColor)
let path = CGPath(roundedRect: rect.insetBy(dx: UIScreenPixel / 2.0, dy: UIScreenPixel / 2.0), cornerWidth: 6.0, cornerHeight: 6.0, transform: nil)
context.addPath(path)
context.strokePath()
}
private func renderIcon(name: String) -> UIImage? {
return generateImage(CGSize(width: 29.0, height: 29.0), contextGenerator: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
if let image = UIImage(bundleImageName: name)?.cgImage {
context.draw(image, in: bounds)
}
drawBorder(context: context, rect: bounds)
})
}
public struct PresentationResourcesSettings {
public static let editProfile = renderIcon(name: "Settings/MenuIcons/EditProfile")
public static let proxy = renderIcon(name: "Settings/MenuIcons/Proxy")
public static let savedMessages = renderIcon(name: "Settings/MenuIcons/SavedMessages")
public static let recentCalls = renderIcon(name: "Settings/MenuIcons/RecentCalls")
public static let stickers = renderIcon(name: "Settings/MenuIcons/Stickers")
public static let notifications = renderIcon(name: "Settings/MenuIcons/Notifications")
public static let security = renderIcon(name: "Settings/MenuIcons/Security")
public static let dataAndStorage = renderIcon(name: "Settings/MenuIcons/DataAndStorage")
public static let appearance = renderIcon(name: "Settings/MenuIcons/Appearance")
public static let language = renderIcon(name: "Settings/MenuIcons/Language")
public static let wallet = generateImage(CGSize(width: 29.0, height: 29.0), contextGenerator: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
context.setFillColor(UIColor.white.cgColor)
context.fill(bounds.insetBy(dx: 5.0, dy: 5.0))
if let image = generateTintedImage(image: UIImage(bundleImageName: "Settings/MenuIcons/Wallet"), color: UIColor(rgb: 0x1b1b1c))?.cgImage {
context.draw(image, in: bounds)
}
drawBorder(context: context, rect: bounds)
})
public static let passport = renderIcon(name: "Settings/MenuIcons/Passport")
public static let watch = renderIcon(name: "Settings/MenuIcons/Watch")
public static let support = renderIcon(name: "Settings/MenuIcons/Support")
public static let faq = renderIcon(name: "Settings/MenuIcons/Faq")
public static let addAccount = renderIcon(name: "Settings/MenuIcons/AddAccount")
public static let setPasscode = renderIcon(name: "Settings/MenuIcons/SetPasscode")
public static let clearCache = renderIcon(name: "Settings/MenuIcons/ClearCache")
public static let changePhoneNumber = renderIcon(name: "Settings/MenuIcons/ChangePhoneNumber")
}

View File

@ -47,34 +47,41 @@ public func stringForFullDate(timestamp: Int32, strings: PresentationStrings, da
var timeinfo = tm()
localtime_r(&t, &timeinfo);
let dayString = "\(timeinfo.tm_mday)"
let yearString = "\(2000 + timeinfo.tm_year - 100)"
let timeString = stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), dateTimeFormat: dateTimeFormat)
let monthFormat: (String, String, String) -> (String, [(Int, NSRange)])
switch timeinfo.tm_mon + 1 {
case 1:
return strings.Time_PreciseDate_m1("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), dateTimeFormat: dateTimeFormat)).0
monthFormat = strings.Time_PreciseDate_m1
case 2:
return strings.Time_PreciseDate_m2("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), dateTimeFormat: dateTimeFormat)).0
monthFormat = strings.Time_PreciseDate_m2
case 3:
return strings.Time_PreciseDate_m3("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), dateTimeFormat: dateTimeFormat)).0
monthFormat = strings.Time_PreciseDate_m3
case 4:
return strings.Time_PreciseDate_m4("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), dateTimeFormat: dateTimeFormat)).0
monthFormat = strings.Time_PreciseDate_m4
case 5:
return strings.Time_PreciseDate_m5("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), dateTimeFormat: dateTimeFormat)).0
monthFormat = strings.Time_PreciseDate_m5
case 6:
return strings.Time_PreciseDate_m6("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), dateTimeFormat: dateTimeFormat)).0
monthFormat = strings.Time_PreciseDate_m6
case 7:
return strings.Time_PreciseDate_m7("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), dateTimeFormat: dateTimeFormat)).0
monthFormat = strings.Time_PreciseDate_m7
case 8:
return strings.Time_PreciseDate_m8("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), dateTimeFormat: dateTimeFormat)).0
monthFormat = strings.Time_PreciseDate_m8
case 9:
return strings.Time_PreciseDate_m9("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), dateTimeFormat: dateTimeFormat)).0
monthFormat = strings.Time_PreciseDate_m9
case 10:
return strings.Time_PreciseDate_m10("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), dateTimeFormat: dateTimeFormat)).0
monthFormat = strings.Time_PreciseDate_m10
case 11:
return strings.Time_PreciseDate_m11("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), dateTimeFormat: dateTimeFormat)).0
monthFormat = strings.Time_PreciseDate_m11
case 12:
return strings.Time_PreciseDate_m12("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), dateTimeFormat: dateTimeFormat)).0
monthFormat = strings.Time_PreciseDate_m12
default:
return ""
}
return monthFormat(dayString, yearString, timeString).0
}
public func stringForDate(timestamp: Int32, strings: PresentationStrings) -> String {

View File

@ -2,7 +2,7 @@
"images" : [
{
"idiom" : "universal",
"filename" : "gramicon.pdf"
"filename" : "ic_wallet.pdf"
}
],
"info" : {

View File

@ -161,7 +161,7 @@ final class ChatSearchInputPanelNode: ChatInputPanelNode {
}
self.membersButton.isHidden = (!(interfaceState.search?.query.isEmpty ?? true)) || self.displayActivity || !canSearchMembers
let resultsEnabled = (resultCount ?? 0) > 5
let resultsEnabled = (resultCount ?? 0) > 0
self.resultsButton.setTitle(resultsText ?? "", with: labelFont, with: resultsEnabled ? interfaceState.theme.chat.inputPanel.panelControlAccentColor : interfaceState.theme.chat.inputPanel.primaryTextColor, for: .normal)
self.resultsButton.isUserInteractionEnabled = resultsEnabled

View File

@ -136,6 +136,8 @@ final class WalletContextImpl: WalletContext {
self.presentationData = WalletPresentationData(
theme: WalletTheme(
info: WalletInfoTheme(
buttonBackgroundColor: UIColor(rgb: 0x32aafe),
buttonTextColor: .white,
incomingFundsTitleColor: theme.chatList.secretTitleColor,
outgoingFundsTitleColor: theme.list.itemDestructiveColor
), setup: WalletSetupTheme(

View File

@ -110,6 +110,7 @@ class ItemListController: ViewController, KeyShortcutResponder, PresentableContr
private var theme: WalletTheme
private var strings: WalletStrings
private var hasNavigationBarSeparator: Bool
private var validLayout: ContainerViewLayout?
@ -212,7 +213,7 @@ class ItemListController: ViewController, KeyShortcutResponder, PresentableContr
var willDisappear: ((Bool) -> Void)?
var didDisappear: ((Bool) -> Void)?
init<ItemGenerationArguments>(theme: WalletTheme, strings: WalletStrings, updatedPresentationData: Signal<(theme: WalletTheme, strings: WalletStrings), NoError>, state: Signal<(ItemListControllerState, (ItemListNodeState, ItemGenerationArguments)), NoError>, tabBarItem: Signal<ItemListControllerTabBarItem, NoError>?) {
init<ItemGenerationArguments>(theme: WalletTheme, strings: WalletStrings, updatedPresentationData: Signal<(theme: WalletTheme, strings: WalletStrings), NoError>, state: Signal<(ItemListControllerState, (ItemListNodeState, ItemGenerationArguments)), NoError>, tabBarItem: Signal<ItemListControllerTabBarItem, NoError>?, hasNavigationBarSeparator: Bool = true) {
self.state = state
|> map { controllerState, nodeStateAndArgument -> (ItemListControllerState, (ItemListNodeState, Any)) in
return (controllerState, (nodeStateAndArgument.0, nodeStateAndArgument.1))
@ -220,8 +221,16 @@ class ItemListController: ViewController, KeyShortcutResponder, PresentableContr
self.theme = theme
self.strings = strings
self.hasNavigationBarSeparator = hasNavigationBarSeparator
super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: theme.navigationBar, strings: NavigationBarStrings(back: strings.Wallet_Navigation_Back, close: strings.Wallet_Navigation_Close)))
let navigationBarTheme: NavigationBarTheme
if hasNavigationBarSeparator {
navigationBarTheme = theme.navigationBar
} else {
navigationBarTheme = NavigationBarTheme(buttonColor: theme.navigationBar.buttonColor, disabledButtonColor: theme.navigationBar.disabledButtonColor, primaryTextColor: theme.navigationBar.primaryTextColor, backgroundColor: theme.list.itemBlocksBackgroundColor, separatorColor: theme.list.itemBlocksBackgroundColor, badgeBackgroundColor: theme.navigationBar.badgeBackgroundColor, badgeStrokeColor: theme.navigationBar.badgeStrokeColor, badgeTextColor: theme.navigationBar.badgeTextColor)
}
super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: navigationBarTheme, strings: NavigationBarStrings(back: strings.Wallet_Navigation_Back, close: strings.Wallet_Navigation_Close)))
self.isOpaqueWhenInOverlay = true
self.blocksBackgroundWhenInOverlay = true
@ -381,7 +390,14 @@ class ItemListController: ViewController, KeyShortcutResponder, PresentableContr
if strongSelf.theme !== controllerState.theme {
strongSelf.theme = controllerState.theme
strongSelf.navigationBar?.updatePresentationData(NavigationBarPresentationData(theme: strongSelf.theme.navigationBar, strings: NavigationBarStrings(back: strongSelf.strings.Wallet_Navigation_Back, close: strongSelf.strings.Wallet_Navigation_Close)))
let navigationBarTheme: NavigationBarTheme
if strongSelf.hasNavigationBarSeparator {
navigationBarTheme = strongSelf.theme.navigationBar
} else {
navigationBarTheme = NavigationBarTheme(buttonColor: strongSelf.theme.navigationBar.buttonColor, disabledButtonColor: strongSelf.theme.navigationBar.disabledButtonColor, primaryTextColor: strongSelf.theme.navigationBar.primaryTextColor, backgroundColor: strongSelf.theme.list.itemBlocksBackgroundColor, separatorColor: strongSelf.theme.list.itemBlocksBackgroundColor, badgeBackgroundColor: strongSelf.theme.navigationBar.badgeBackgroundColor, badgeStrokeColor: strongSelf.theme.navigationBar.badgeStrokeColor, badgeTextColor: strongSelf.theme.navigationBar.badgeTextColor)
}
strongSelf.navigationBar?.updatePresentationData(NavigationBarPresentationData(theme: navigationBarTheme, strings: NavigationBarStrings(back: strongSelf.strings.Wallet_Navigation_Back, close: strongSelf.strings.Wallet_Navigation_Close)))
strongSelf.statusBar.statusBarStyle = strongSelf.theme.statusBarStyle
var items = strongSelf.navigationItem.rightBarButtonItems ?? []

View File

@ -0,0 +1,292 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import AnimatedStickerNode
import SwiftSignalKit
import AppBundle
class WalletAmountItem: ListViewItem, ItemListItem {
let theme: WalletTheme
let amount: String
let sectionId: ItemListSectionId
let textUpdated: (String) -> Void
let shouldUpdateText: (String) -> Bool
let processPaste: ((String) -> String)?
let updatedFocus: ((Bool) -> Void)?
let tag: ItemListItemTag?
init(theme: WalletTheme, amount: String, sectionId: ItemListSectionId, textUpdated: @escaping (String) -> Void, shouldUpdateText: @escaping (String) -> Bool = { _ in return true }, processPaste: ((String) -> String)? = nil, updatedFocus: ((Bool) -> Void)? = nil, tag: ItemListItemTag? = nil) {
self.theme = theme
self.amount = amount
self.sectionId = sectionId
self.textUpdated = textUpdated
self.shouldUpdateText = shouldUpdateText
self.processPaste = processPaste
self.updatedFocus = updatedFocus
self.tag = tag
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = WalletAmountItemNode()
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 {
if let nodeValue = node() as? WalletAmountItemNode {
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 integralFont = Font.medium(48.0)
private let fractionalFont = Font.medium(24.0)
class WalletAmountItemNode: ListViewItemNode, UITextFieldDelegate, ItemListItemNode, ItemListItemFocusableNode {
private let backgroundNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let textNode: TextFieldNode
private let iconNode: AnimatedStickerNode
private let measureNode: TextNode
private var item: WalletAmountItem?
var tag: ItemListItemTag? {
return self.item?.tag
}
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.textNode = TextFieldNode()
self.iconNode = AnimatedStickerNode()
if let path = getAppBundle().path(forResource: "WalletIntroStatic", ofType: "tgs") {
self.iconNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 120, height: 120, mode: .direct)
self.iconNode.visibility = true
}
self.measureNode = TextNode()
super.init(layerBacked: false, dynamicBounce: false)
self.clipsToBounds = false
self.addSubnode(self.textNode)
self.addSubnode(self.iconNode)
}
override func didLoad() {
super.didLoad()
self.textNode.textField.textAlignment = .center
self.textNode.textField.typingAttributes = [NSAttributedString.Key.font: integralFont]
self.textNode.textField.font = integralFont
if let item = self.item {
self.textNode.textField.textColor = item.theme.list.itemPrimaryTextColor
self.textNode.textField.keyboardAppearance = item.theme.keyboardAppearance
self.textNode.textField.tintColor = item.theme.list.itemAccentColor
//self.textNode.textField.accessibilityHint = item.placeholder
}
self.textNode.clipsToBounds = true
self.textNode.textField.delegate = self
self.textNode.textField.addTarget(self, action: #selector(self.textFieldTextChanged(_:)), for: .editingChanged)
self.textNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0)
}
func asyncLayout() -> (_ item: WalletAmountItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let currentItem = self.item
let makeMeasureLayout = TextNode.asyncLayout(self.measureNode)
return { item, params, neighbors in
var updatedTheme: WalletTheme?
if currentItem?.theme !== item.theme {
updatedTheme = item.theme
}
let leftInset: CGFloat = 16.0 + params.leftInset
var rightInset: CGFloat = 16.0 + params.rightInset
let separatorHeight = UIScreenPixel
let contentSize = CGSize(width: params.width, height: 100.0)
var insets = itemListNeighborsGroupedInsets(neighbors)
insets.top = 0.0
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
let attributedPlaceholderText = NSAttributedString(string: "0", font: integralFont, textColor: item.theme.list.itemPlaceholderTextColor)
let attributedAmountText = amountAttributedString(item.amount, integralFont: integralFont, fractionalFont: fractionalFont, color: item.theme.list.itemPrimaryTextColor)
let (measureLayout, _) = makeMeasureLayout(TextNodeLayoutArguments(attributedString: item.amount.isEmpty ? attributedPlaceholderText : attributedAmountText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
if let _ = updatedTheme {
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
strongSelf.textNode.textField.textColor = item.theme.list.itemPrimaryTextColor
strongSelf.textNode.textField.keyboardAppearance = item.theme.keyboardAppearance
strongSelf.textNode.textField.tintColor = item.theme.list.itemAccentColor
}
let capitalizationType = UITextAutocapitalizationType.none
let autocorrectionType = UITextAutocorrectionType.no
let keyboardType = UIKeyboardType.decimalPad
if strongSelf.textNode.textField.keyboardType != keyboardType {
strongSelf.textNode.textField.keyboardType = keyboardType
}
if strongSelf.textNode.textField.autocapitalizationType != capitalizationType {
strongSelf.textNode.textField.autocapitalizationType = capitalizationType
}
if strongSelf.textNode.textField.autocorrectionType != autocorrectionType {
strongSelf.textNode.textField.autocorrectionType = autocorrectionType
}
if let currentText = strongSelf.textNode.textField.text {
if currentText != item.amount {
strongSelf.textNode.textField.attributedText = attributedAmountText
}
} else {
strongSelf.textNode.textField.attributedText = attributedAmountText
}
let iconSize = CGSize(width: 50.0, height: 50.0)
let verticalOffset: CGFloat = -10.0
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floor((layout.contentSize.height - 48.0) / 2.0) + verticalOffset), size: CGSize(width: max(1.0, params.width - (leftInset + rightInset) + iconSize.width - 5.0), height: 48.0))
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 1)
}
let bottomStripeInset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = leftInset
default:
bottomStripeInset = 0.0
strongSelf.bottomStripeNode.isHidden = false
}
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - UIScreenPixel), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
if strongSelf.textNode.textField.attributedPlaceholder == nil || !strongSelf.textNode.textField.attributedPlaceholder!.isEqual(to: attributedPlaceholderText) {
strongSelf.textNode.textField.attributedPlaceholder = attributedPlaceholderText
strongSelf.textNode.textField.accessibilityHint = attributedPlaceholderText.string
}
let iconFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - max(31.0, measureLayout.size.width)) / 2.0 - 28.0), y: floor((layout.contentSize.height - iconSize.height) / 2.0) - 3.0 + verticalOffset), size: iconSize)
strongSelf.iconNode.updateLayout(size: iconFrame.size)
strongSelf.iconNode.frame = iconFrame
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
@objc private func textFieldTextChanged(_ textField: UITextField) {
self.textUpdated(self.textNode.textField.text ?? "")
}
@objc private func clearButtonPressed() {
self.textNode.textField.text = ""
self.textUpdated("")
}
private func textUpdated(_ text: String) {
self.item?.textUpdated(text)
}
func focus() {
if !self.textNode.textField.isFirstResponder {
self.textNode.textField.becomeFirstResponder()
}
}
@objc func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let newText = ((textField.text ?? "") as NSString).replacingCharacters(in: range, with: string)
if let item = self.item {
if !item.shouldUpdateText(newText) {
return false
}
}
if string.count > 1, let item = self.item, let processPaste = item.processPaste {
let result = processPaste(string)
if result != string {
var text = textField.text ?? ""
text.replaceSubrange(text.index(text.startIndex, offsetBy: range.lowerBound) ..< text.index(text.startIndex, offsetBy: range.upperBound), with: result)
textField.attributedText = amountAttributedString(text, integralFont: integralFont, fractionalFont: fractionalFont, color: item.theme.list.itemPrimaryTextColor)
if let startPosition = textField.position(from: textField.beginningOfDocument, offset: range.lowerBound + result.count) {
let selectionRange = textField.textRange(from: startPosition, to: startPosition)
DispatchQueue.main.async {
textField.selectedTextRange = selectionRange
}
}
self.textFieldTextChanged(textField)
return false
}
}
if let item = self.item {
textField.attributedText = amountAttributedString(newText, integralFont: integralFont, fractionalFont: fractionalFont, color: item.theme.list.itemPrimaryTextColor)
self.textFieldTextChanged(textField)
return false
} else {
return true
}
}
@objc func textFieldDidBeginEditing(_ textField: UITextField) {
self.item?.updatedFocus?(true)
}
@objc func textFieldDidEndEditing(_ textField: UITextField) {
self.item?.updatedFocus?(false)
}
}

View File

@ -0,0 +1,168 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import Markdown
class WalletBalanceItem: ListViewItem, ItemListItem {
let theme: WalletTheme
let title: String
let value: String
let insufficient: Bool
let sectionId: ItemListSectionId
let style: ItemListStyle
let isAlwaysPlain: Bool = true
init(theme: WalletTheme, title: String, value: String, insufficient: Bool, sectionId: ItemListSectionId, style: ItemListStyle = .blocks) {
self.theme = theme
self.title = title
self.value = value
self.insufficient = insufficient
self.sectionId = sectionId
self.style = style
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = WalletBalanceItemNode()
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? WalletBalanceItemNode 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 transactionIcon = UIImage(bundleImageName: "Wallet/TransactionGem")?.precomposed()
class WalletBalanceItemNode: ListViewItemNode {
private let titleNode: TextNode
private let valueNode: TextNode
private let iconNode: ASImageNode
private let activateArea: AccessibilityAreaNode
private var item: WalletBalanceItem?
init() {
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.contentMode = .left
self.titleNode.contentsScale = UIScreen.main.scale
self.valueNode = TextNode()
self.valueNode.isUserInteractionEnabled = false
self.valueNode.contentMode = .left
self.valueNode.contentsScale = UIScreen.main.scale
self.iconNode = ASImageNode()
self.iconNode.displaysAsynchronously = false
self.iconNode.displayWithoutProcessing = true
self.activateArea = AccessibilityAreaNode()
self.activateArea.accessibilityTraits = .staticText
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.titleNode)
self.addSubnode(self.valueNode)
self.addSubnode(self.iconNode)
self.addSubnode(self.activateArea)
}
func asyncLayout() -> (_ item: WalletBalanceItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeValueLayout = TextNode.asyncLayout(self.valueNode)
let currentItem = self.item
return { item, params, neighbors in
var updatedTheme: WalletTheme?
if currentItem?.theme !== item.theme {
updatedTheme = item.theme
}
let leftInset: CGFloat = 15.0 + params.leftInset
let verticalInset: CGFloat = 7.0
let iconImage: UIImage? = transactionIcon
let iconSize = CGSize(width: 12.0, height: 10.0)
let textColor = item.insufficient ? item.theme.list.freeTextErrorColor : item.theme.list.freeTextColor
let attributedTitle = NSAttributedString(string: item.title, font: titleFont, textColor: textColor)
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: attributedTitle, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let attributedValue = NSAttributedString(string: item.value, font: titleFont, textColor: textColor)
let (valueLayout, valueApply) = makeValueLayout(TextNodeLayoutArguments(attributedString: attributedValue, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let contentSize: CGSize
contentSize = CGSize(width: params.width, height: titleLayout.size.height + verticalInset + verticalInset)
let insets = itemListNeighborsGroupedInsets(neighbors)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height))
if let _ = updatedTheme {
strongSelf.iconNode.image = iconImage
}
let accessibilityLabel = item.title + item.value
strongSelf.activateArea.accessibilityLabel = accessibilityLabel
strongSelf.accessibilityLabel = accessibilityLabel
let _ = titleApply()
let _ = valueApply()
let iconSpacing: CGFloat = 3.0
let valueSpacing: CGFloat = 2.0
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: titleLayout.size)
strongSelf.valueNode.frame = CGRect(origin: CGPoint(x: leftInset + titleLayout.size.width + iconSpacing + iconSize.width + valueSpacing, y: verticalInset), size: valueLayout.size)
strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: leftInset + titleLayout.size.width + iconSpacing, y: verticalInset + 3.0), size: iconSize)
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}

View File

@ -10,24 +10,14 @@ private final class WalletCreateInvoiceScreenArguments {
let context: WalletContext
let updateState: ((WalletCreateInvoiceScreenState) -> WalletCreateInvoiceScreenState) -> Void
let updateText: (WalletCreateInvoiceScreenEntryTag, String) -> Void
let selectNextInputItem: (WalletCreateInvoiceScreenEntryTag) -> Void
let dismissInput: () -> Void
let copyAddress: () -> Void
let shareAddressLink: () -> Void
let openQrCode: () -> Void
let displayQrCodeContextMenu: () -> Void
let scrollToBottom: () -> Void
init(context: WalletContext, updateState: @escaping ((WalletCreateInvoiceScreenState) -> WalletCreateInvoiceScreenState) -> Void, updateText: @escaping (WalletCreateInvoiceScreenEntryTag, String) -> Void, selectNextInputItem: @escaping (WalletCreateInvoiceScreenEntryTag) -> Void, dismissInput: @escaping () -> Void, copyAddress: @escaping () -> Void, shareAddressLink: @escaping () -> Void, openQrCode: @escaping () -> Void, displayQrCodeContextMenu: @escaping () -> Void, scrollToBottom: @escaping () -> Void) {
init(context: WalletContext, updateState: @escaping ((WalletCreateInvoiceScreenState) -> WalletCreateInvoiceScreenState) -> Void, updateText: @escaping (WalletCreateInvoiceScreenEntryTag, String) -> Void, dismissInput: @escaping () -> Void, scrollToBottom: @escaping () -> Void) {
self.context = context
self.updateState = updateState
self.updateText = updateText
self.selectNextInputItem = selectNextInputItem
self.dismissInput = dismissInput
self.copyAddress = copyAddress
self.shareAddressLink = shareAddressLink
self.openQrCode = openQrCode
self.displayQrCodeContextMenu = displayQrCodeContextMenu
self.scrollToBottom = scrollToBottom
}
}
@ -35,7 +25,6 @@ private final class WalletCreateInvoiceScreenArguments {
private enum WalletCreateInvoiceScreenSection: Int32 {
case amount
case comment
case address
}
private enum WalletCreateInvoiceScreenEntryTag: ItemListItemTag {
@ -52,63 +41,43 @@ private enum WalletCreateInvoiceScreenEntryTag: ItemListItemTag {
}
private enum WalletCreateInvoiceScreenEntry: ItemListNodeEntry {
case amountHeader(WalletTheme, String)
case amount(WalletTheme, WalletStrings, String, String)
case amount(WalletTheme, String)
case amountInfo(WalletTheme, String)
case commentHeader(WalletTheme, String)
case comment(WalletTheme, String, String)
case addressCode(WalletTheme, String)
case addressHeader(WalletTheme, String)
case address(WalletTheme, String, Bool)
case copyAddress(WalletTheme, String)
case shareAddressLink(WalletTheme, String)
case addressInfo(WalletTheme, String)
var section: ItemListSectionId {
switch self {
case .amountHeader, .amount:
case .amount, .amountInfo:
return WalletCreateInvoiceScreenSection.amount.rawValue
case .commentHeader, .comment:
return WalletCreateInvoiceScreenSection.comment.rawValue
case .addressCode, .addressHeader, .address, .copyAddress, .shareAddressLink, .addressInfo:
return WalletCreateInvoiceScreenSection.address.rawValue
}
}
var stableId: Int32 {
switch self {
case .amountHeader:
return 0
case .amount:
return 0
case .amountInfo:
return 1
case .commentHeader:
return 2
case .comment:
return 3
case .addressCode:
return 4
case .addressHeader:
return 5
case .address:
return 6
case .copyAddress:
return 7
case .shareAddressLink:
return 8
case .addressInfo:
return 9
}
}
static func ==(lhs: WalletCreateInvoiceScreenEntry, rhs: WalletCreateInvoiceScreenEntry) -> Bool {
switch lhs {
case let .amountHeader(lhsTheme, lhsText):
if case let .amountHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
case let .amount(lhsTheme, lhsAmount):
if case let .amount(rhsTheme, rhsAmount) = rhs, lhsTheme === rhsTheme, lhsAmount == rhsAmount {
return true
} else {
return false
}
case let .amount(lhsTheme, lhsStrings, lhsPlaceholder, lhsBalance):
if case let .amount(rhsTheme, rhsStrings, rhsPlaceholder, rhsBalance) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsPlaceholder == rhsPlaceholder, lhsBalance == rhsBalance {
case let .amountInfo(lhsTheme, lhsText):
if case let .amountInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
@ -125,42 +94,6 @@ private enum WalletCreateInvoiceScreenEntry: ItemListNodeEntry {
} else {
return false
}
case let .addressCode(lhsTheme, lhsAddress):
if case let .addressCode(rhsTheme, rhsAddress) = rhs, lhsTheme === rhsTheme, lhsAddress == rhsAddress {
return true
} else {
return false
}
case let .addressHeader(lhsTheme, lhsText):
if case let .addressHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .address(lhsTheme, lhsAddress, lhsMonospace):
if case let .address(rhsTheme, rhsAddress, rhsMonospace) = rhs, lhsTheme === rhsTheme, lhsAddress == rhsAddress, lhsMonospace == rhsMonospace {
return true
} else {
return false
}
case let .copyAddress(lhsTheme, lhsText):
if case let .copyAddress(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .shareAddressLink(lhsTheme, lhsText):
if case let .shareAddressLink(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .addressInfo(lhsTheme, lhsText):
if case let .addressInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
}
}
@ -171,10 +104,8 @@ private enum WalletCreateInvoiceScreenEntry: ItemListNodeEntry {
func item(_ arguments: Any) -> ListViewItem {
let arguments = arguments as! WalletCreateInvoiceScreenArguments
switch self {
case let .amountHeader(theme, text):
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
case let .amount(theme, strings, placeholder, text):
return ItemListSingleLineInputItem(theme: theme, strings: strings, title: NSAttributedString(string: ""), text: text, placeholder: placeholder, type: .decimal, returnKeyType: .next, clearType: .onFocus, tag: WalletCreateInvoiceScreenEntryTag.amount, sectionId: self.section, textUpdated: { text in
case let .amount(theme, amount):
return WalletAmountItem(theme: theme, amount: amount, sectionId: self.section, textUpdated: { text in
let text = formatAmountText(text, decimalSeparator: arguments.context.presentationData.dateTimeFormat.decimalSeparator)
arguments.updateText(WalletCreateInvoiceScreenEntryTag.amount, text)
}, shouldUpdateText: { text in
@ -183,7 +114,7 @@ private enum WalletCreateInvoiceScreenEntry: ItemListNodeEntry {
if isValidAmount(pastedText) {
return normalizedStringForGramsString(pastedText)
} else {
return text
return amount
}
}, updatedFocus: { focus in
arguments.updateState { state in
@ -191,9 +122,7 @@ private enum WalletCreateInvoiceScreenEntry: ItemListNodeEntry {
state.focusItemTag = focus ? WalletCreateInvoiceScreenEntryTag.amount : nil
return state
}
if focus {
arguments.scrollToBottom()
} else {
if !focus {
let presentationData = arguments.context.presentationData
arguments.updateState { state in
var state = state
@ -203,13 +132,13 @@ private enum WalletCreateInvoiceScreenEntry: ItemListNodeEntry {
return state
}
}
}, action: {
arguments.selectNextInputItem(WalletCreateInvoiceScreenEntryTag.amount)
})
}, tag: WalletCreateInvoiceScreenEntryTag.amount)
case let .amountInfo(theme, text):
return ItemListTextItem(theme: theme, text: .markdown(text), sectionId: self.section)
case let .commentHeader(theme, text):
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
case let .comment(theme, placeholder, value):
return ItemListMultilineInputItem(theme: theme, text: value, placeholder: placeholder, maxLength: ItemListMultilineInputItemTextLimit(value: walletTextLimit, display: true), sectionId: self.section, style: .blocks, returnKeyType: .done, textUpdated: { text in
return ItemListMultilineInputItem(theme: theme, text: value, placeholder: placeholder, maxLength: ItemListMultilineInputItemTextLimit(value: walletTextLimit, display: true, mode: .bytes), sectionId: self.section, style: .blocks, returnKeyType: .done, textUpdated: { text in
arguments.updateText(WalletCreateInvoiceScreenEntryTag.comment, text)
}, shouldUpdateText: { text in
let textLength: Int = text.data(using: .utf8, allowLossyConversion: true)?.count ?? 0
@ -226,26 +155,6 @@ private enum WalletCreateInvoiceScreenEntry: ItemListNodeEntry {
}, tag: WalletCreateInvoiceScreenEntryTag.comment, action: {
arguments.dismissInput()
})
case let .addressCode(theme, text):
return WalletQrCodeItem(theme: theme, address: text, sectionId: self.section, style: .blocks, action: {
arguments.openQrCode()
}, longTapAction: {
arguments.displayQrCodeContextMenu()
})
case let .addressHeader(theme, text):
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
case let .address(theme, text, monospace):
return ItemListMultilineTextItem(theme: theme, text: text, font: monospace ? .monospace : .default, sectionId: self.section, style: .blocks)
case let .copyAddress(theme, text):
return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.copyAddress()
})
case let .shareAddressLink(theme, text):
return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.shareAddressLink()
})
case let .addressInfo(theme, text):
return ItemListTextItem(theme: theme, text: .markdown(text), sectionId: self.section)
}
}
}
@ -262,23 +171,10 @@ private struct WalletCreateInvoiceScreenState: Equatable {
private func walletCreateInvoiceScreenEntries(presentationData: WalletPresentationData, address: String, state: WalletCreateInvoiceScreenState) -> [WalletCreateInvoiceScreenEntry] {
var entries: [WalletCreateInvoiceScreenEntry] = []
let amount = amountValue(state.amount)
entries.append(.amountHeader(presentationData.theme, presentationData.strings.Wallet_Receive_AmountHeader))
entries.append(.amount(presentationData.theme, presentationData.strings, presentationData.strings.Wallet_Receive_AmountText, state.amount ?? ""))
entries.append(.amount(presentationData.theme, state.amount ?? ""))
entries.append(.amountInfo(presentationData.theme, presentationData.strings.Wallet_Receive_CreateInvoiceInfo))
entries.append(.commentHeader(presentationData.theme, presentationData.strings.Wallet_Receive_CommentHeader))
entries.append(.comment(presentationData.theme, presentationData.strings.Wallet_Receive_CommentInfo, state.comment))
let url = walletInvoiceUrl(address: address, amount: state.amount, comment: state.comment)
entries.append(.addressCode(presentationData.theme, url))
entries.append(.addressHeader(presentationData.theme, presentationData.strings.Wallet_Receive_InvoiceUrlHeader))
entries.append(.address(presentationData.theme, url, false))
entries.append(.copyAddress(presentationData.theme, presentationData.strings.Wallet_Receive_CopyInvoiceUrl))
entries.append(.shareAddressLink(presentationData.theme, presentationData.strings.Wallet_Receive_ShareInvoiceUrl))
entries.append(.addressInfo(presentationData.theme, presentationData.strings.Wallet_Receive_ShareUrlInfo))
return entries
}
@ -287,6 +183,9 @@ protocol WalletCreateInvoiceScreen {
}
private final class WalletCreateInvoiceScreenImpl: ItemListController, WalletCreateInvoiceScreen {
override func preferredContentSizeForLayout(_ layout: ContainerViewLayout) -> CGSize? {
return CGSize(width: layout.size.width, height: layout.size.height - 174.0)
}
}
func walletCreateInvoiceScreen(context: WalletContext, address: String) -> ViewController {
@ -300,10 +199,8 @@ func walletCreateInvoiceScreen(context: WalletContext, address: String) -> ViewC
var presentControllerImpl: ((ViewController, Any?) -> Void)?
var pushImpl: ((ViewController) -> Void)?
var dismissImpl: (() -> Void)?
var selectNextInputItemImpl: ((WalletCreateInvoiceScreenEntryTag) -> Void)?
var dismissInputImpl: (() -> Void)?
var ensureItemVisibleImpl: ((WalletCreateInvoiceScreenEntryTag, Bool) -> Void)?
var displayQrCodeContextMenuImpl: (() -> Void)?
weak var currentStatusController: ViewController?
let arguments = WalletCreateInvoiceScreenArguments(context: context, updateState: { f in
@ -319,59 +216,40 @@ func walletCreateInvoiceScreen(context: WalletContext, address: String) -> ViewC
}
return state
}
ensureItemVisibleImpl?(WalletCreateInvoiceScreenEntryTag.comment, false)
}, selectNextInputItem: { tag in
selectNextInputItemImpl?(tag)
ensureItemVisibleImpl?(tag, false)
}, dismissInput: {
dismissInputImpl?()
}, copyAddress: {
let presentationData = context.presentationData
let state = stateValue.with { $0 }
UIPasteboard.general.string = walletInvoiceUrl(address: address, amount: state.amount, comment: state.comment)
if currentStatusController == nil {
let statusController = OverlayStatusController(theme: presentationData.theme, type: .genericSuccess(presentationData.strings.Wallet_Receive_InvoiceUrlCopied, false))
presentControllerImpl?(statusController, nil)
currentStatusController = statusController
}
}, shareAddressLink: {
dismissInputImpl?()
let state = stateValue.with { $0 }
let url = walletInvoiceUrl(address: address, amount: state.amount, comment: state.comment)
context.shareUrl(url)
}, openQrCode: {
dismissInputImpl?()
let state = stateValue.with { $0 }
let url = walletInvoiceUrl(address: address, amount: state.amount, comment: state.comment)
pushImpl?(WalletQrViewScreen(context: context, invoice: url))
}, displayQrCodeContextMenu: {
dismissInputImpl?()
displayQrCodeContextMenuImpl?()
}, scrollToBottom: {
ensureItemVisibleImpl?(WalletCreateInvoiceScreenEntryTag.comment, true)
})
let signal = combineLatest(queue: .mainQueue(), .single(context.presentationData), statePromise.get())
|> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
var ensureVisibleItemTag: ItemListItemTag?
if let focusItemTag = state.focusItemTag {
ensureVisibleItemTag = focusItemTag
}
let rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Wallet_Navigation_Done), style: .bold, enabled: true, action: {
dismissImpl?()
})
let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.Wallet_CreateInvoice_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Wallet_Navigation_Back), animateChanges: false)
let listState = ItemListNodeState(entries: walletCreateInvoiceScreenEntries(presentationData: presentationData, address: address, state: state), style: .blocks, ensureVisibleItemTag: ensureVisibleItemTag, animateChanges: false)
return (controllerState, (listState, arguments))
|> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
var ensureVisibleItemTag: ItemListItemTag?
if let focusItemTag = state.focusItemTag {
ensureVisibleItemTag = focusItemTag
}
let rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Wallet_Navigation_Done), style: .bold, enabled: !state.isEmpty, action: {
pushImpl?(WalletReceiveScreen(context: context, mode: .invoice(address: address, amount: state.amount, comment: state.comment)))
})
let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.Wallet_CreateInvoice_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Wallet_Navigation_Back), animateChanges: false)
let listState = ItemListNodeState(entries: walletCreateInvoiceScreenEntries(presentationData: presentationData, address: address, state: state), style: .blocks, focusItemTag: ensureVisibleItemTag, ensureVisibleItemTag: ensureVisibleItemTag, animateChanges: false)
return (controllerState, (listState, arguments))
}
let controller = WalletCreateInvoiceScreenImpl(theme: context.presentationData.theme, strings: context.presentationData.strings, updatedPresentationData: .single((context.presentationData.theme, context.presentationData.strings)), state: signal, tabBarItem: nil)
let controller = WalletCreateInvoiceScreenImpl(theme: context.presentationData.theme, strings: context.presentationData.strings, updatedPresentationData: .single((context.presentationData.theme, context.presentationData.strings)), state: signal, tabBarItem: nil, hasNavigationBarSeparator: false)
controller.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
controller.experimentalSnapScrollToItem = true
controller.didAppear = { _ in
updateState { state in
var state = state
state.focusItemTag = .amount
return state
}
}
presentControllerImpl = { [weak controller] c, a in
controller?.present(c, in: .window(.root), with: a)
}
@ -382,27 +260,6 @@ func walletCreateInvoiceScreen(context: WalletContext, address: String) -> ViewC
controller?.view.endEditing(true)
let _ = controller?.dismiss()
}
selectNextInputItemImpl = { [weak controller] currentTag in
guard let controller = controller else {
return
}
var resultItemNode: ItemListItemFocusableNode?
var focusOnNext = false
let _ = controller.frameForItemNode({ itemNode in
if let itemNode = itemNode as? ItemListItemNode, let tag = itemNode.tag, let focusableItemNode = itemNode as? ItemListItemFocusableNode {
if focusOnNext && resultItemNode == nil {
resultItemNode = focusableItemNode
return true
} else if currentTag.isEqual(to: tag) {
focusOnNext = true
}
}
return false
})
if let resultItemNode = resultItemNode {
resultItemNode.focus()
}
}
dismissInputImpl = { [weak controller] in
controller?.view.endEditing(true)
}
@ -427,10 +284,5 @@ func walletCreateInvoiceScreen(context: WalletContext, address: String) -> ViewC
}
})
}
displayQrCodeContextMenuImpl = { [weak controller] in
let state = stateValue.with { $0 }
let url = walletInvoiceUrl(address: address, amount: state.amount, comment: state.comment)
shareInvoiceQrCode(context: context, invoice: url)
}
return controller
}

View File

@ -9,6 +9,39 @@ 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?
@ -44,6 +77,8 @@ public final class WalletInfoScreen: ViewController {
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()
}
@ -75,12 +110,12 @@ public final class WalletInfoScreen: ViewController {
guard let strongSelf = self, let walletInfo = strongSelf.walletInfo else {
return
}
strongSelf.push(walletReceiveScreen(context: strongSelf.context, address: strongSelf.address))
strongSelf.push(WalletReceiveScreen(context: strongSelf.context, mode: .receive(address: strongSelf.address)))
}, openTransaction: { [weak self] transaction in
guard let strongSelf = self else {
return
}
strongSelf.push(walletTransactionInfoController(context: strongSelf.context, walletInfo: strongSelf.walletInfo, walletTransaction: transaction, enableDebugActions: strongSelf.enableDebugActions))
strongSelf.push(WalletTransactionInfoScreen(context: strongSelf.context, walletInfo: strongSelf.walletInfo, walletTransaction: transaction, enableDebugActions: strongSelf.enableDebugActions))
}, present: { [weak self] c, a in
guard let strongSelf = self else {
return
@ -229,9 +264,9 @@ private final class WalletInfoHeaderNode: ASDisplayNode {
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: UIImage(bundleImageName: "Wallet/ReceiveButtonIcon"), theme: SolidRoundedButtonTheme(backgroundColor: .white, foregroundColor: .black), height: 50.0, cornerRadius: 10.0, gloss: false)
self.receiveGramsButtonNode = SolidRoundedButtonNode(title: presentationData.strings.Wallet_Info_ReceiveGrams, icon: UIImage(bundleImageName: "Wallet/ReceiveButtonIcon"), theme: SolidRoundedButtonTheme(backgroundColor: .white, foregroundColor: .black), height: 50.0, cornerRadius: 10.0, gloss: false)
self.sendButtonNode = SolidRoundedButtonNode(title: presentationData.strings.Wallet_Info_Send, icon: UIImage(bundleImageName: "Wallet/SendButtonIcon"), theme: SolidRoundedButtonTheme(backgroundColor: .white, foregroundColor: .black), height: 50.0, cornerRadius: 10.0, gloss: false)
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)

View File

@ -376,7 +376,7 @@ class WalletInfoTransactionItemNode: ListViewItemNode {
let iconFrame = CGRect(origin: CGPoint(x: titleSignFrame.maxX + (titleSignFrame.width.isZero ? 0.0 : 1.0), y: titleSignFrame.minY + floor((titleLayout.size.height - iconSize.height) / 2.0) - 1.0), size: iconSize)
strongSelf.iconNode.frame = iconFrame
let titleFrame = CGRect(origin: CGPoint(x: iconFrame.maxX + 1.0, y: topInset), size: titleLayout.size)
let titleFrame = CGRect(origin: CGPoint(x: iconFrame.maxX + 3.0, y: topInset), size: titleLayout.size)
strongSelf.titleNode.frame = titleFrame
let directionFrame = CGRect(origin: CGPoint(x: titleFrame.maxX + 3.0, y: titleFrame.maxY - directionLayout.size.height - 1.0), size: directionLayout.size)

View File

@ -31,13 +31,19 @@ public struct WalletPresentationDateTimeFormat: Equatable {
}
public final class WalletInfoTheme {
public let buttonBackgroundColor: UIColor
public let buttonTextColor: UIColor
public let incomingFundsTitleColor: UIColor
public let outgoingFundsTitleColor: UIColor
public init(
buttonBackgroundColor: UIColor,
buttonTextColor: UIColor,
incomingFundsTitleColor: UIColor,
outgoingFundsTitleColor: UIColor
) {
self.buttonBackgroundColor = buttonBackgroundColor
self.buttonTextColor = buttonTextColor
self.incomingFundsTitleColor = incomingFundsTitleColor
self.outgoingFundsTitleColor = outgoingFundsTitleColor
}

View File

@ -7,23 +7,6 @@ import Display
import QrCode
import AnimatedStickerNode
func shareInvoiceQrCode(context: WalletContext, invoice: String) {
let _ = (qrCode(string: invoice, color: .black, backgroundColor: .white, icon: .custom(UIImage(bundleImageName: "Wallet/QrGem")))
|> map { generator -> UIImage? in
let imageSize = CGSize(width: 768.0, height: 768.0)
let context = generator(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), scale: 1.0))
return context?.generateImage()
}
|> deliverOnMainQueue).start(next: { image in
guard let image = image else {
return
}
let activityController = UIActivityViewController(activityItems: [image], applicationActivities: nil)
context.presentNativeController(activityController)
})
}
public final class WalletQrViewScreen: ViewController {
private let context: WalletContext
private let invoice: String
@ -104,7 +87,7 @@ public final class WalletQrViewScreen: ViewController {
}
@objc private func shareButtonPressed() {
shareInvoiceQrCode(context: self.context, invoice: self.invoice)
//shareInvoiceQrCode(context: self.context, invoice: self.invoice)
}
}

View File

@ -1,245 +1,314 @@
import Foundation
import UIKit
import SwiftSignalKit
import AppBundle
import AsyncDisplayKit
import Display
import SwiftSignalKit
import OverlayStatusController
import QrCode
import AnimatedStickerNode
import SolidRoundedButtonNode
private final class WalletReceiveScreenArguments {
let context: WalletContext
let copyAddress: () -> Void
let shareAddressLink: () -> Void
let openQrCode: () -> Void
let displayQrCodeContextMenu: () -> Void
let openCreateInvoice: () -> Void
init(context: WalletContext, copyAddress: @escaping () -> Void, shareAddressLink: @escaping () -> Void, openQrCode: @escaping () -> Void, displayQrCodeContextMenu: @escaping () -> Void, openCreateInvoice: @escaping () -> Void) {
self.context = context
self.copyAddress = copyAddress
self.shareAddressLink = shareAddressLink
self.openQrCode = openQrCode
self.displayQrCodeContextMenu = displayQrCodeContextMenu
self.openCreateInvoice = openCreateInvoice
private func shareInvoiceQrCode(context: WalletContext, invoice: String) {
let _ = (qrCode(string: invoice, color: .black, backgroundColor: .white, icon: .custom(UIImage(bundleImageName: "Wallet/QrGem")))
|> map { generator -> UIImage? in
let imageSize = CGSize(width: 768.0, height: 768.0)
let context = generator(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), scale: 1.0))
return context?.generateImage()
}
}
private enum WalletReceiveScreenSection: Int32 {
case address
case invoice
}
private enum WalletReceiveScreenEntry: ItemListNodeEntry {
case addressCode(WalletTheme, String)
case addressHeader(WalletTheme, String)
case address(WalletTheme, String, Bool)
case copyAddress(WalletTheme, String)
case shareAddressLink(WalletTheme, String)
case addressInfo(WalletTheme, String)
case invoice(WalletTheme, String)
case invoiceInfo(WalletTheme, String)
var section: ItemListSectionId {
switch self {
case .addressCode, .addressHeader, .address, .copyAddress, .shareAddressLink, .addressInfo:
return WalletReceiveScreenSection.address.rawValue
case .invoice, .invoiceInfo:
return WalletReceiveScreenSection.invoice.rawValue
|> deliverOnMainQueue).start(next: { image in
guard let image = image else {
return
}
}
var stableId: Int32 {
switch self {
case .addressCode:
return 0
case .addressHeader:
return 1
case .address:
return 2
case .copyAddress:
return 3
case .shareAddressLink:
return 4
case .addressInfo:
return 5
case .invoice:
return 6
case .invoiceInfo:
return 7
}
}
static func ==(lhs: WalletReceiveScreenEntry, rhs: WalletReceiveScreenEntry) -> Bool {
switch lhs {
case let .addressCode(lhsTheme, lhsAddress):
if case let .addressCode(rhsTheme, rhsAddress) = rhs, lhsTheme === rhsTheme, lhsAddress == rhsAddress {
return true
} else {
return false
}
case let .addressHeader(lhsTheme, lhsText):
if case let .addressHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .address(lhsTheme, lhsAddress, lhsMonospace):
if case let .address(rhsTheme, rhsAddress, rhsMonospace) = rhs, lhsTheme === rhsTheme, lhsAddress == rhsAddress, lhsMonospace == rhsMonospace {
return true
} else {
return false
}
case let .copyAddress(lhsTheme, lhsText):
if case let .copyAddress(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .shareAddressLink(lhsTheme, lhsText):
if case let .shareAddressLink(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .addressInfo(lhsTheme, lhsText):
if case let .addressInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .invoice(lhsTheme, lhsText):
if case let .invoice(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .invoiceInfo(lhsTheme, lhsText):
if case let .invoiceInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
}
}
static func <(lhs: WalletReceiveScreenEntry, rhs: WalletReceiveScreenEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(_ arguments: Any) -> ListViewItem {
let arguments = arguments as! WalletReceiveScreenArguments
switch self {
case let .addressCode(theme, text):
return WalletQrCodeItem(theme: theme, address: text, sectionId: self.section, style: .blocks, action: {
arguments.openQrCode()
}, longTapAction: {
arguments.displayQrCodeContextMenu()
})
case let .addressHeader(theme, text):
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
case let .address(theme, text, monospace):
return ItemListMultilineTextItem(theme: theme, text: text, font: monospace ? .monospace : .default, sectionId: self.section, style: .blocks)
case let .copyAddress(theme, text):
return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.copyAddress()
})
case let .shareAddressLink(theme, text):
return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.shareAddressLink()
})
case let .addressInfo(theme, text):
return ItemListTextItem(theme: theme, text: .markdown(text), sectionId: self.section)
case let .invoice(theme, text):
return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.openCreateInvoice()
})
case let .invoiceInfo(theme, text):
return ItemListTextItem(theme: theme, text: .markdown(text), sectionId: self.section)
}
}
}
private func walletReceiveScreenEntries(presentationData: WalletPresentationData, address: String) -> [WalletReceiveScreenEntry] {
var entries: [WalletReceiveScreenEntry] = []
entries.append(.addressCode(presentationData.theme, walletInvoiceUrl(address: address)))
entries.append(.addressHeader(presentationData.theme, presentationData.strings.Wallet_Receive_AddressHeader))
entries.append(.address(presentationData.theme, formatAddress(address), true))
entries.append(.copyAddress(presentationData.theme, presentationData.strings.Wallet_Receive_CopyAddress))
entries.append(.shareAddressLink(presentationData.theme, presentationData.strings.Wallet_Receive_ShareAddress))
entries.append(.addressInfo(presentationData.theme, presentationData.strings.Wallet_Receive_ShareUrlInfo))
entries.append(.invoice(presentationData.theme, presentationData.strings.Wallet_Receive_CreateInvoice))
entries.append(.invoiceInfo(presentationData.theme, presentationData.strings.Wallet_Receive_CreateInvoiceInfo))
return entries
}
protocol WalletReceiveScreen {
}
private final class WalletReceiveScreenImpl: ItemListController, WalletReceiveScreen {
}
func walletReceiveScreen(context: WalletContext, address: String) -> ViewController {
var presentControllerImpl: ((ViewController, Any?) -> Void)?
var pushImpl: ((ViewController) -> Void)?
var dismissImpl: (() -> Void)?
var displayQrCodeContextMenuImpl: (() -> Void)?
weak var currentStatusController: ViewController?
let arguments = WalletReceiveScreenArguments(context: context, copyAddress: {
let presentationData = context.presentationData
UIPasteboard.general.string = address
if currentStatusController == nil {
let statusController = OverlayStatusController(theme: presentationData.theme, type: .genericSuccess(presentationData.strings.Wallet_Receive_AddressCopied, false))
presentControllerImpl?(statusController, nil)
currentStatusController = statusController
}
}, shareAddressLink: {
context.shareUrl(walletInvoiceUrl(address: address))
}, openQrCode: {
let url = walletInvoiceUrl(address: address)
pushImpl?(WalletQrViewScreen(context: context, invoice: url))
}, displayQrCodeContextMenu: {
displayQrCodeContextMenuImpl?()
}, openCreateInvoice: {
pushImpl?(walletCreateInvoiceScreen(context: context, address: address))
let activityController = UIActivityViewController(activityItems: [image], applicationActivities: nil)
context.presentNativeController(activityController)
})
let signal = Signal<WalletPresentationData, NoError>.single(context.presentationData)
|> deliverOnMainQueue
|> map { presentationData -> (ItemListControllerState, (ItemListNodeState, Any)) in
let rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Wallet_Navigation_Done), style: .bold, enabled: true, action: {
dismissImpl?()
})
let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.Wallet_Receive_Title), leftNavigationButton: ItemListNavigationButton(content: .none, style: .regular, enabled: false, action: {}), rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Wallet_Navigation_Back), animateChanges: false)
let listState = ItemListNodeState(entries: walletReceiveScreenEntries(presentationData: presentationData, address: address), style: .blocks, animateChanges: false)
return (controllerState, (listState, arguments))
}
let controller = WalletReceiveScreenImpl(theme: context.presentationData.theme, strings: context.presentationData.strings, updatedPresentationData: .single((context.presentationData.theme, context.presentationData.strings)), state: signal, tabBarItem: nil)
controller.navigationPresentation = .modal
controller.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
presentControllerImpl = { [weak controller] c, a in
controller?.present(c, in: .window(.root), with: a)
}
pushImpl = { [weak controller] c in
controller?.push(c)
}
dismissImpl = { [weak controller] in
controller?.view.endEditing(true)
let _ = controller?.dismiss()
}
displayQrCodeContextMenuImpl = { [weak controller] in
let url = walletInvoiceUrl(address: address)
shareInvoiceQrCode(context: context, invoice: url)
}
return controller
}
public enum WalletReceiveScreenMode {
case receive(address: String)
case invoice(address: String, amount: String?, comment: String?)
var address: String {
switch self {
case let .receive(address), let .invoice(address, _, _):
return address
}
}
}
final class WalletReceiveScreen: ViewController {
private let context: WalletContext
private let mode: WalletReceiveScreenMode
private var presentationData: WalletPresentationData
private var previousScreenBrightness: CGFloat?
private var displayLinkAnimator: DisplayLinkAnimator?
private let idleTimerExtensionDisposable: Disposable
public init(context: WalletContext, mode: WalletReceiveScreenMode) {
self.context = context
self.mode = mode
self.presentationData = context.presentationData
let defaultTheme = self.presentationData.theme.navigationBar
let navigationBarTheme = NavigationBarTheme(buttonColor: defaultTheme.buttonColor, disabledButtonColor: defaultTheme.disabledButtonColor, primaryTextColor: defaultTheme.primaryTextColor, backgroundColor: .clear, separatorColor: .clear, badgeBackgroundColor: defaultTheme.badgeBackgroundColor, badgeStrokeColor: defaultTheme.badgeStrokeColor, badgeTextColor: defaultTheme.badgeTextColor)
self.idleTimerExtensionDisposable = context.idleTimerExtension()
super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: navigationBarTheme, strings: NavigationBarStrings(back: self.presentationData.strings.Wallet_Navigation_Back, close: self.presentationData.strings.Wallet_Navigation_Close)))
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
self.navigationBar?.intrinsicCanTransitionInline = false
if case .receive = mode {
self.navigationPresentation = .flatModal
self.navigationItem.leftBarButtonItem = UIBarButtonItem(customView: UIView())
}
self.title = self.presentationData.strings.Wallet_Receive_Title
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Wallet_Navigation_Back, style: .plain, target: nil, action: nil)
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Wallet_Navigation_Done, style: .done, target: self, action: #selector(self.donePressed))
}
deinit {
self.idleTimerExtensionDisposable.dispose()
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func loadDisplayNode() {
self.displayNode = WalletReceiveScreenNode(context: self.context, presentationData: self.presentationData, mode: self.mode)
(self.displayNode as! WalletReceiveScreenNode).openCreateInvoice = { [weak self] in
guard let strongSelf = self else {
return
}
self?.push(walletCreateInvoiceScreen(context: strongSelf.context, address: strongSelf.mode.address))
}
self.displayNodeDidLoad()
}
override public func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let screenBrightness = UIScreen.main.brightness
if screenBrightness < 0.85 {
self.previousScreenBrightness = screenBrightness
self.displayLinkAnimator = DisplayLinkAnimator(duration: 0.5, from: screenBrightness, to: 0.85, update: { value in
UIScreen.main.brightness = value
}, completion: {
self.displayLinkAnimator = nil
})
}
}
public override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
let screenBrightness = UIScreen.main.brightness
if let previousScreenBrightness = self.previousScreenBrightness, screenBrightness > previousScreenBrightness {
self.displayLinkAnimator = DisplayLinkAnimator(duration: 0.2, from: screenBrightness, to: previousScreenBrightness, update: { value in
UIScreen.main.brightness = value
}, completion: {
self.displayLinkAnimator = nil
})
}
}
override func preferredContentSizeForLayout(_ layout: ContainerViewLayout) -> CGSize? {
return CGSize(width: layout.size.width, height: layout.size.height - 174.0)
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
(self.displayNode as! WalletReceiveScreenNode).containerLayoutUpdated(layout: layout, navigationHeight: self.navigationHeight, transition: transition)
}
@objc private func donePressed() {
if let navigationController = self.navigationController as? NavigationController {
var controllers = navigationController.viewControllers
controllers = controllers.filter { controller in
if controller is WalletReceiveScreen {
return false
}
if controller is WalletCreateInvoiceScreen {
return false
}
return true
}
navigationController.setViewControllers(controllers, animated: true)
}
}
}
private func urlForMode(_ mode: WalletReceiveScreenMode) -> String {
switch mode {
case let .receive(address):
return walletInvoiceUrl(address: address)
case let .invoice(address, amount, comment):
return walletInvoiceUrl(address: address, amount: amount, comment: comment)
}
}
private final class WalletReceiveScreenNode: ViewControllerTracingNode {
private let context: WalletContext
private var presentationData: WalletPresentationData
private let mode: WalletReceiveScreenMode
private let textNode: ImmediateTextNode
private let qrButtonNode: HighlightTrackingButtonNode
private let qrImageNode: TransformImageNode
private let qrIconNode: AnimatedStickerNode
private let urlTextNode: ImmediateTextNode
private let buttonNode: SolidRoundedButtonNode
private let secondaryButtonNode: HighlightableButtonNode
var openCreateInvoice: (() -> Void)?
init(context: WalletContext, presentationData: WalletPresentationData, mode: WalletReceiveScreenMode) {
self.context = context
self.presentationData = presentationData
self.mode = mode
self.textNode = ImmediateTextNode()
self.textNode.textAlignment = .center
self.textNode.maximumNumberOfLines = 3
self.qrImageNode = TransformImageNode()
self.qrImageNode.clipsToBounds = true
self.qrImageNode.cornerRadius = 14.0
self.qrIconNode = AnimatedStickerNode()
if let path = getAppBundle().path(forResource: "WalletIntroStatic", ofType: "tgs") {
self.qrIconNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 240, height: 240, mode: .direct)
self.qrIconNode.visibility = true
}
self.qrButtonNode = HighlightTrackingButtonNode()
self.urlTextNode = ImmediateTextNode()
self.urlTextNode.maximumNumberOfLines = 4
self.urlTextNode.textAlignment = .justified
self.urlTextNode.lineSpacing = 0.35
self.buttonNode = SolidRoundedButtonNode(title: "", icon: nil, theme: SolidRoundedButtonTheme(backgroundColor: self.presentationData.theme.setup.buttonFillColor, foregroundColor: self.presentationData.theme.setup.buttonForegroundColor), height: 50.0, cornerRadius: 10.0, gloss: false)
self.secondaryButtonNode = HighlightableButtonNode()
super.init()
self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
self.addSubnode(self.textNode)
self.addSubnode(self.qrImageNode)
self.addSubnode(self.qrIconNode)
self.addSubnode(self.qrButtonNode)
self.addSubnode(self.urlTextNode)
self.addSubnode(self.buttonNode)
self.addSubnode(self.secondaryButtonNode)
self.qrImageNode.setSignal(qrCode(string: urlForMode(mode), color: .black, backgroundColor: .white, icon: .cutout), attemptSynchronously: true)
self.qrButtonNode.addTarget(self, action: #selector(self.qrPressed), forControlEvents: .touchUpInside)
self.qrButtonNode.highligthedChanged = { [weak self] highlighted in
guard let strongSelf = self else {
return
}
if highlighted {
strongSelf.qrImageNode.alpha = 0.4
strongSelf.qrIconNode.alpha = 0.4
} else {
strongSelf.qrImageNode.layer.animateAlpha(from: strongSelf.qrImageNode.alpha, to: 1.0, duration: 0.2)
strongSelf.qrIconNode.layer.animateAlpha(from: strongSelf.qrIconNode.alpha, to: 1.0, duration: 0.2)
strongSelf.qrImageNode.alpha = 1.0
strongSelf.qrIconNode.alpha = 1.0
}
}
let textFont = Font.regular(16.0)
let addressFont = Font.monospace(17.0)
let textColor = self.presentationData.theme.list.itemPrimaryTextColor
let secondaryTextColor = self.presentationData.theme.list.itemSecondaryTextColor
let url = urlForMode(self.mode)
switch self.mode {
case let .receive(address):
self.textNode.attributedText = NSAttributedString(string: self.presentationData.strings.Wallet_Receive_ShareUrlInfo, font: textFont, textColor: secondaryTextColor)
self.urlTextNode.attributedText = NSAttributedString(string: formatAddress(url + " "), font: addressFont, textColor: textColor, paragraphAlignment: .justified)
self.buttonNode.title = self.presentationData.strings.Wallet_Receive_ShareAddress
self.secondaryButtonNode.setTitle(self.presentationData.strings.Wallet_Receive_CreateInvoice, with: Font.regular(17.0), with: self.presentationData.theme.list.itemAccentColor, for: .normal)
case let .invoice(address, amount, comment):
self.textNode.attributedText = NSAttributedString(string: self.presentationData.strings.Wallet_Receive_ShareUrlInfo, font: textFont, textColor: secondaryTextColor, paragraphAlignment: .center)
let sliced = String(url.enumerated().map { $0 > 0 && $0 % 32 == 0 ? ["\n", $1] : [$1]}.joined())
self.urlTextNode.attributedText = NSAttributedString(string: sliced, font: addressFont, textColor: textColor, paragraphAlignment: .justified)
self.buttonNode.title = self.presentationData.strings.Wallet_Receive_ShareInvoiceUrl
}
self.buttonNode.pressed = {
context.shareUrl(url)
}
self.secondaryButtonNode.addTarget(self, action: #selector(createInvoicePressed), forControlEvents: .touchUpInside)
}
@objc private func qrPressed() {
shareInvoiceQrCode(context: self.context, invoice: urlForMode(self.mode))
}
@objc private func createInvoicePressed() {
self.openCreateInvoice?()
}
func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) {
var insets = layout.insets(options: [])
insets.top += navigationHeight
let inset: CGFloat = 22.0
let textSize = self.textNode.updateLayout(CGSize(width: layout.size.width - inset * 2.0, height: CGFloat.greatestFiniteMagnitude))
let textFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - textSize.width) / 2.0), y: insets.top + 24.0), size: textSize)
transition.updateFrame(node: self.textNode, frame: textFrame)
let makeImageLayout = self.qrImageNode.asyncLayout()
let imageSide: CGFloat = 215.0
var imageSize = CGSize(width: imageSide, height: imageSide)
let imageApply = makeImageLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: nil))
let _ = imageApply()
let imageFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - imageSize.width) / 2.0), y: textFrame.maxY + 20.0), size: imageSize)
transition.updateFrame(node: self.qrImageNode, frame: imageFrame)
transition.updateFrame(node: self.qrButtonNode, frame: imageFrame)
let iconSide = floor(imageSide * 0.24)
let iconSize = CGSize(width: iconSide, height: iconSide)
self.qrIconNode.updateLayout(size: iconSize)
transition.updateBounds(node: self.qrIconNode, bounds: CGRect(origin: CGPoint(), size: iconSize))
transition.updatePosition(node: self.qrIconNode, position: imageFrame.center.offsetBy(dx: 0.0, dy: -1.0))
let urlTextSize = self.urlTextNode.updateLayout(CGSize(width: layout.size.width - inset * 2.0, height: CGFloat.greatestFiniteMagnitude))
transition.updateFrame(node: self.urlTextNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - urlTextSize.width) / 2.0), y: imageFrame.maxY + 25.0), size: urlTextSize))
let buttonSideInset: CGFloat = 16.0
let bottomInset = insets.bottom + 10.0
let buttonWidth = layout.size.width - buttonSideInset * 2.0
let buttonHeight: CGFloat = 50.0
var buttonOffset: CGFloat = 0.0
if let _ = self.secondaryButtonNode.attributedTitle(for: .normal) {
buttonOffset = -60.0
self.secondaryButtonNode.frame = CGRect(x: floor((layout.size.width - buttonWidth) / 2.0), y: layout.size.height - bottomInset - buttonHeight, width: buttonWidth, height: buttonHeight)
}
let buttonFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - buttonWidth) / 2.0), y: layout.size.height - bottomInset - buttonHeight + buttonOffset), size: CGSize(width: buttonWidth, height: buttonHeight))
transition.updateFrame(node: self.buttonNode, frame: buttonFrame)
self.buttonNode.updateLayout(width: buttonFrame.width, transition: transition)
}
}

View File

@ -55,20 +55,20 @@ private enum WalletSendScreenEntryTag: ItemListItemTag {
}
private enum WalletSendScreenEntry: ItemListNodeEntry {
case amount(WalletTheme, String)
case balance(WalletTheme, String, String, Bool)
case addressHeader(WalletTheme, String)
case address(WalletTheme, String, String)
case addressInfo(WalletTheme, String)
case amountHeader(WalletTheme, String, String?, Bool)
case amount(WalletTheme, WalletStrings, String, String)
case commentHeader(WalletTheme, String)
case comment(WalletTheme, String, String)
var section: ItemListSectionId {
switch self {
case .amount, .balance:
return WalletSendScreenSection.amount.rawValue
case .addressHeader, .address, .addressInfo:
return WalletSendScreenSection.address.rawValue
case .amountHeader, .amount:
return WalletSendScreenSection.amount.rawValue
case .commentHeader, .comment:
return WalletSendScreenSection.comment.rawValue
}
@ -76,15 +76,15 @@ private enum WalletSendScreenEntry: ItemListNodeEntry {
var stableId: Int32 {
switch self {
case .addressHeader:
return 0
case .address:
return 1
case .addressInfo:
return 2
case .amountHeader:
return 3
case .amount:
return 0
case .balance:
return 1
case .addressHeader:
return 2
case .address:
return 3
case .addressInfo:
return 4
case .commentHeader:
return 5
@ -95,6 +95,18 @@ private enum WalletSendScreenEntry: ItemListNodeEntry {
static func ==(lhs: WalletSendScreenEntry, rhs: WalletSendScreenEntry) -> Bool {
switch lhs {
case let .amount(lhsTheme, lhsAmount):
if case let .amount(rhsTheme, rhsAmount) = rhs, lhsTheme === rhsTheme, lhsAmount == rhsAmount {
return true
} else {
return false
}
case let .balance(lhsTheme, lhsTitle, lhsBalance, lhsInsufficient):
if case let .balance(rhsTheme, rhsTitle, rhsBalance, rhsInsufficient) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsBalance == rhsBalance, lhsInsufficient == rhsInsufficient {
return true
} else {
return false
}
case let .addressHeader(lhsTheme, lhsText):
if case let .addressHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
@ -113,18 +125,6 @@ private enum WalletSendScreenEntry: ItemListNodeEntry {
} else {
return false
}
case let .amountHeader(lhsTheme, lhsText, lhsBalance, lhsInsufficient):
if case let .amountHeader(rhsTheme, rhsText, rhsBalance, rhsInsufficient) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsBalance == rhsBalance, lhsInsufficient == rhsInsufficient {
return true
} else {
return false
}
case let .amount(lhsTheme, lhsStrings, lhsPlaceholder, lhsAmount):
if case let .amount(rhsTheme, rhsStrings, rhsPlaceholder, rhsAmount) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsPlaceholder == rhsPlaceholder, lhsAmount == rhsAmount {
return true
} else {
return false
}
case let .commentHeader(lhsTheme, lhsText):
if case let .commentHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
@ -147,6 +147,33 @@ private enum WalletSendScreenEntry: ItemListNodeEntry {
func item(_ arguments: Any) -> ListViewItem {
let arguments = arguments as! WalletSendScreenArguments
switch self {
case let .amount(theme, text):
return WalletAmountItem(theme: theme, amount: text, sectionId: self.section, textUpdated: { text in
let text = formatAmountText(text, decimalSeparator: arguments.context.presentationData.dateTimeFormat.decimalSeparator)
arguments.updateText(WalletSendScreenEntryTag.amount, text)
}, shouldUpdateText: { text in
return isValidAmount(text)
}, processPaste: { pastedText in
if isValidAmount(pastedText) {
let presentationData = arguments.context.presentationData
return normalizedStringForGramsString(pastedText, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator)
} else {
return text
}
}, updatedFocus: { focus in
if !focus {
let presentationData = arguments.context.presentationData
arguments.updateState { state in
var state = state
if !state.amount.isEmpty {
state.amount = normalizedStringForGramsString(state.amount, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator)
}
return state
}
}
}, tag: WalletSendScreenEntryTag.amount)
case let .balance(theme, title, balance, insufficient):
return WalletBalanceItem(theme: theme, title: title, value: balance, insufficient: insufficient, sectionId: self.section)
case let .addressHeader(theme, text):
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
case let .address(theme, placeholder, address):
@ -190,35 +217,6 @@ private enum WalletSendScreenEntry: ItemListNodeEntry {
}))
case let .addressInfo(theme, text):
return ItemListTextItem(theme: theme, text: .markdown(text), sectionId: self.section)
case let .amountHeader(theme, text, balance, insufficient):
return ItemListSectionHeaderItem(theme: theme, text: text, activityIndicator: balance == nil ? .right : .none, accessoryText: balance.flatMap { ItemListSectionHeaderAccessoryText(value: $0, color: insufficient ? .destructive : .generic, icon: balanceIcon) }, sectionId: self.section)
case let .amount(theme, strings, placeholder, text):
return ItemListSingleLineInputItem(theme: theme, strings: strings, title: NSAttributedString(string: ""), text: text, placeholder: placeholder, type: .decimal, returnKeyType: .next, clearType: .onFocus, tag: WalletSendScreenEntryTag.amount, sectionId: self.section, textUpdated: { text in
let text = formatAmountText(text, decimalSeparator: arguments.context.presentationData.dateTimeFormat.decimalSeparator)
arguments.updateText(WalletSendScreenEntryTag.amount, text)
}, shouldUpdateText: { text in
return isValidAmount(text)
}, processPaste: { pastedText in
if isValidAmount(pastedText) {
let presentationData = arguments.context.presentationData
return normalizedStringForGramsString(pastedText, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator)
} else {
return text
}
}, updatedFocus: { focus in
if !focus {
let presentationData = arguments.context.presentationData
arguments.updateState { state in
var state = state
if !state.amount.isEmpty {
state.amount = normalizedStringForGramsString(state.amount, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator)
}
return state
}
}
}, action: {
arguments.selectNextInputItem(WalletSendScreenEntryTag.amount)
})
case let .commentHeader(theme, text):
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
case let .comment(theme, placeholder, value):
@ -247,15 +245,15 @@ private func walletSendScreenEntries(presentationData: WalletPresentationData, b
}
var entries: [WalletSendScreenEntry] = []
let amount = amountValue(state.amount)
let balance = max(0, balance ?? 0)
entries.append(.amount(presentationData.theme, state.amount ?? ""))
entries.append(.balance(presentationData.theme, presentationData.strings.Wallet_Send_Balance("").0, formatBalanceText(balance, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator), balance == 0 || (amount > 0 && balance < amount)))
entries.append(.addressHeader(presentationData.theme, presentationData.strings.Wallet_Send_AddressHeader))
entries.append(.address(presentationData.theme, presentationData.strings.Wallet_Send_AddressText, state.address))
entries.append(.addressInfo(presentationData.theme, presentationData.strings.Wallet_Send_AddressInfo))
let amount = amountValue(state.amount)
let balance = max(0, balance ?? 0)
entries.append(.amountHeader(presentationData.theme, presentationData.strings.Wallet_Receive_AmountHeader, presentationData.strings.Wallet_Send_Balance(formatBalanceText(balance, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator)).0, balance == 0 || (amount > 0 && balance < amount)))
entries.append(.amount(presentationData.theme, presentationData.strings, presentationData.strings.Wallet_Send_AmountText, state.amount ?? ""))
entries.append(.commentHeader(presentationData.theme, presentationData.strings.Wallet_Receive_CommentHeader))
entries.append(.comment(presentationData.theme, presentationData.strings.Wallet_Receive_CommentInfo, state.comment))
return entries
@ -450,10 +448,10 @@ public func walletSendScreen(context: WalletContext, randomId: Int64, walletInfo
}
var focusItemTag: ItemListItemTag?
if address == nil {
focusItemTag = WalletSendScreenEntryTag.address
} else if amount == nil {
if amount == nil {
focusItemTag = WalletSendScreenEntryTag.amount
} else if address == nil {
focusItemTag = WalletSendScreenEntryTag.address
}
let signal = combineLatest(queue: .mainQueue(), .single(context.presentationData), walletState, statePromise.get())
@ -485,7 +483,7 @@ public func walletSendScreen(context: WalletContext, randomId: Int64, walletInfo
return (controllerState, (listState, arguments))
}
let controller = WalletSendScreenImpl(theme: context.presentationData.theme, strings: context.presentationData.strings, updatedPresentationData: .single((context.presentationData.theme, context.presentationData.strings)), state: signal, tabBarItem: nil)
let controller = WalletSendScreenImpl(theme: context.presentationData.theme, strings: context.presentationData.strings, updatedPresentationData: .single((context.presentationData.theme, context.presentationData.strings)), state: signal, tabBarItem: nil, hasNavigationBarSeparator: false)
controller.navigationPresentation = .modal
controller.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
presentControllerImpl = { [weak controller] c, a in

View File

@ -385,6 +385,9 @@ public final class WalletSplashScreen: ViewController {
if controller is WalletWordCheckScreen {
return false
}
if controller is WalletTransactionInfoScreen {
return false
}
return true
}

View File

@ -7,196 +7,48 @@ import SolidRoundedButtonNode
import SwiftSignalKit
import OverlayStatusController
import WalletCore
import AnimatedStickerNode
private func stringForFullDate(timestamp: Int32, strings: WalletStrings, dateTimeFormat: WalletPresentationDateTimeFormat) -> String {
var t: time_t = Int(timestamp)
var timeinfo = tm()
localtime_r(&t, &timeinfo);
let dayString = "\(timeinfo.tm_mday)"
let yearString = "\(2000 + timeinfo.tm_year - 100)"
let timeString = stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), dateTimeFormat: dateTimeFormat)
let monthFormat: (String, String, String) -> (String, [(Int, NSRange)])
switch timeinfo.tm_mon + 1 {
case 1:
return strings.Wallet_Time_PreciseDate_m1("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), dateTimeFormat: dateTimeFormat)).0
monthFormat = strings.Wallet_Time_PreciseDate_m1
case 2:
return strings.Wallet_Time_PreciseDate_m2("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), dateTimeFormat: dateTimeFormat)).0
monthFormat = strings.Wallet_Time_PreciseDate_m2
case 3:
return strings.Wallet_Time_PreciseDate_m3("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), dateTimeFormat: dateTimeFormat)).0
monthFormat = strings.Wallet_Time_PreciseDate_m3
case 4:
return strings.Wallet_Time_PreciseDate_m4("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), dateTimeFormat: dateTimeFormat)).0
monthFormat = strings.Wallet_Time_PreciseDate_m4
case 5:
return strings.Wallet_Time_PreciseDate_m5("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), dateTimeFormat: dateTimeFormat)).0
monthFormat = strings.Wallet_Time_PreciseDate_m5
case 6:
return strings.Wallet_Time_PreciseDate_m6("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), dateTimeFormat: dateTimeFormat)).0
monthFormat = strings.Wallet_Time_PreciseDate_m6
case 7:
return strings.Wallet_Time_PreciseDate_m7("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), dateTimeFormat: dateTimeFormat)).0
monthFormat = strings.Wallet_Time_PreciseDate_m7
case 8:
return strings.Wallet_Time_PreciseDate_m8("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), dateTimeFormat: dateTimeFormat)).0
monthFormat = strings.Wallet_Time_PreciseDate_m8
case 9:
return strings.Wallet_Time_PreciseDate_m9("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), dateTimeFormat: dateTimeFormat)).0
monthFormat = strings.Wallet_Time_PreciseDate_m9
case 10:
return strings.Wallet_Time_PreciseDate_m10("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), dateTimeFormat: dateTimeFormat)).0
monthFormat = strings.Wallet_Time_PreciseDate_m10
case 11:
return strings.Wallet_Time_PreciseDate_m11("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), dateTimeFormat: dateTimeFormat)).0
monthFormat = strings.Wallet_Time_PreciseDate_m11
case 12:
return strings.Wallet_Time_PreciseDate_m12("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), dateTimeFormat: dateTimeFormat)).0
monthFormat = strings.Wallet_Time_PreciseDate_m12
default:
return ""
}
}
private final class WalletTransactionInfoControllerArguments {
let copyWalletAddress: () -> Void
let sendGrams: () -> Void
let displayContextMenu: (WalletTransactionInfoEntryTag, String) -> Void
let openFeeInfo: () -> Void
init(copyWalletAddress: @escaping () -> Void, sendGrams: @escaping () -> Void, displayContextMenu: @escaping (WalletTransactionInfoEntryTag, String) -> Void, openFeeInfo: @escaping () -> Void) {
self.copyWalletAddress = copyWalletAddress
self.sendGrams = sendGrams
self.displayContextMenu = displayContextMenu
self.openFeeInfo = openFeeInfo
}
}
private enum WalletTransactionInfoSection: Int32 {
case amount
case info
case storageFee
case otherFee
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(WalletTheme, WalletStrings, WalletPresentationDateTimeFormat, WalletInfoTransaction)
case infoHeader(WalletTheme, String)
case infoAddress(WalletTheme, String, String?)
case infoCopyAddress(WalletTheme, String)
case infoSendGrams(WalletTheme, String)
case storageFeeHeader(WalletTheme, String)
case storageFee(WalletTheme, String)
case storageFeeInfo(WalletTheme, String)
case otherFeeHeader(WalletTheme, String)
case otherFee(WalletTheme, String)
case otherFeeInfo(WalletTheme, String)
case commentHeader(WalletTheme, String)
case comment(WalletTheme, String)
var section: ItemListSectionId {
switch self {
case .amount:
return WalletTransactionInfoSection.amount.rawValue
case .infoHeader, .infoAddress, .infoCopyAddress, .infoSendGrams:
return WalletTransactionInfoSection.info.rawValue
case .storageFeeHeader, .storageFee, .storageFeeInfo:
return WalletTransactionInfoSection.storageFee.rawValue
case .otherFeeHeader, .otherFee, .otherFeeInfo:
return WalletTransactionInfoSection.otherFee.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
case .storageFeeHeader:
return 7
case .storageFee:
return 8
case .storageFeeInfo:
return 9
case .otherFeeHeader:
return 10
case .otherFee:
return 11
case .otherFeeInfo:
return 12
}
}
static func <(lhs: WalletTransactionInfoEntry, rhs: WalletTransactionInfoEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(_ arguments: Any) -> ListViewItem {
let arguments = arguments as! WalletTransactionInfoControllerArguments
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, 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 .storageFeeHeader(theme, text):
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
case let .storageFee(theme, text):
return ItemListMultilineTextItem(theme: theme, text: text, sectionId: self.section, style: .blocks, longTapAction: nil, tag: nil)
case let .storageFeeInfo(theme, text):
return ItemListTextItem(theme: theme, text: .markdown(text), sectionId: self.section, linkAction: { action in
switch action {
case .tap:
arguments.openFeeInfo()
}
}, style: .blocks)
case let .otherFeeHeader(theme, text):
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
case let .otherFee(theme, text):
return ItemListMultilineTextItem(theme: theme, text: text, sectionId: self.section, style: .blocks, longTapAction: nil, tag: nil)
case let .otherFeeInfo(theme, text):
return ItemListTextItem(theme: theme, text: .markdown(text), sectionId: self.section, linkAction: { action in
switch action {
case .tap:
arguments.openFeeInfo()
}
}, style: .blocks)
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, sectionId: self.section, style: .blocks, longTapAction: {
arguments.displayContextMenu(WalletTransactionInfoEntryTag.comment, text)
}, tag: WalletTransactionInfoEntryTag.comment)
}
}
}
private struct WalletTransactionInfoControllerState: Equatable {
return monthFormat(dayString, yearString, timeString).0
}
private enum WalletTransactionAddress {
@ -266,359 +118,339 @@ private func extractDescription(_ walletTransaction: WalletInfoTransaction) -> S
}
}
private func walletTransactionInfoControllerEntries(presentationData: WalletPresentationData, walletTransaction: WalletInfoTransaction, state: WalletTransactionInfoControllerState, walletInfo: WalletInfo?) -> [WalletTransactionInfoEntry] {
var entries: [WalletTransactionInfoEntry] = []
private func messageBubbleImage(incoming: Bool, fillColor: UIColor, strokeColor: UIColor) -> UIImage {
let diameter: CGFloat = 36.0
let corner: CGFloat = 7.0
entries.append(.amount(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, walletTransaction))
let transferredValue: Int64
switch walletTransaction {
case let .completed(transaction):
transferredValue = transaction.transferredValueWithoutFees
case let .pending(transaction):
transferredValue = -transaction.value
}
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))
}
if case let .completed(transaction) = walletTransaction {
if transaction.storageFee != 0 {
entries.append(.storageFeeHeader(presentationData.theme, presentationData.strings.Wallet_TransactionInfo_StorageFeeHeader))
entries.append(.storageFee(presentationData.theme, formatBalanceText(-transaction.storageFee, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator)))
entries.append(.storageFeeInfo(presentationData.theme, presentationData.strings.Wallet_TransactionInfo_StorageFeeInfo))
}
if transaction.otherFee != 0 {
entries.append(.otherFeeHeader(presentationData.theme, presentationData.strings.Wallet_TransactionInfo_OtherFeeHeader))
entries.append(.otherFee(presentationData.theme, formatBalanceText(-transaction.otherFee, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator)))
entries.append(.otherFeeInfo(presentationData.theme, presentationData.strings.Wallet_TransactionInfo_OtherFeeInfo))
}
}
return entries
}
func walletTransactionInfoController(context: WalletContext, walletInfo: WalletInfo?, walletTransaction: WalletInfoTransaction, 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.presentationData
presentControllerImpl?(OverlayStatusController(theme: presentationData.theme, 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?()
var randomId: Int64 = 0
arc4random_buf(&randomId, 8)
pushImpl?(walletSendScreen(context: context, randomId: randomId, walletInfo: walletInfo, address: address))
}
}, displayContextMenu: { tag, text in
displayContextMenuImpl?(tag, text)
}, openFeeInfo: {
let presentationData = context.presentationData
context.openUrl(presentationData.strings.Wallet_TransactionInfo_FeeInfoURL)
})
let signal = combineLatest(queue: .mainQueue(), .single(context.presentationData), statePromise.get())
|> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.Wallet_TransactionInfo_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Wallet_Navigation_Back), animateChanges: false)
let listState = ItemListNodeState(entries: walletTransactionInfoControllerEntries(presentationData: presentationData, walletTransaction: walletTransaction, state: state, walletInfo: walletInfo), style: .blocks, animateChanges: false)
return generateImage(CGSize(width: 42.0, height: diameter), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
return (controllerState, (listState, arguments))
}
|> afterDisposed {
}
let controller = ItemListController(theme: context.presentationData.theme, strings: context.presentationData.strings, updatedPresentationData: .single((context.presentationData.theme, context.presentationData.strings)), state: signal, tabBarItem: nil)
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.presentationData
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.Wallet_ContextMenuCopy, accessibilityLabel: presentationData.strings.Wallet_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, walletInfo: nil, address: value, enableDebugActions: enableDebugActions))
}))
}
}
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
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: incoming ? 1.0 : -1.0, y: -1.0)
context.translateBy(x: -size.width / 2.0 + 0.5, y: -size.height / 2.0 + 0.5)
let lineWidth: CGFloat = 1.0
context.setFillColor(fillColor.cgColor)
context.setLineWidth(lineWidth)
context.setStrokeColor(strokeColor.cgColor)
let _ = try? drawSvgPath(context, path: "M6,17.5 C6,7.83289181 13.8350169,0 23.5,0 C33.1671082,0 41,7.83501688 41,17.5 C41,27.1671082 33.1649831,35 23.5,35 C19.2941198,35 15.4354328,33.5169337 12.4179496,31.0453367 C9.05531719,34.9894816 -2.41102995e-08,35 0,35 C5.972003,31.5499861 6,26.8616169 6,26.8616169 L6,17.5 L6,17.5 ")
context.strokePath()
let _ = try? drawSvgPath(context, path: "M6,17.5 C6,7.83289181 13.8350169,0 23.5,0 C33.1671082,0 41,7.83501688 41,17.5 C41,27.1671082 33.1649831,35 23.5,35 C19.2941198,35 15.4354328,33.5169337 12.4179496,31.0453367 C9.05531719,34.9894816 -2.41102995e-08,35 0,35 C5.972003,31.5499861 6,26.8616169 6,26.8616169 L6,17.5 L6,17.5 ")
context.fillPath()
})!.stretchableImage(withLeftCapWidth: incoming ? Int(corner + diameter / 2.0) : Int(diameter / 2.0), topCapHeight: Int(diameter / 2.0))
}
class WalletTransactionHeaderItem: ListViewItem, ItemListItem {
let theme: WalletTheme
let strings: WalletStrings
let dateTimeFormat: WalletPresentationDateTimeFormat
let walletTransaction: WalletInfoTransaction
let sectionId: ItemListSectionId
let isAlwaysPlain: Bool = true
final class WalletTransactionInfoScreen: ViewController {
private let context: WalletContext
private let walletInfo: WalletInfo?
private let walletTransaction: WalletInfoTransaction
private var presentationData: WalletPresentationData
init(theme: WalletTheme, strings: WalletStrings, dateTimeFormat: WalletPresentationDateTimeFormat, walletTransaction: WalletInfoTransaction, sectionId: ItemListSectionId) {
self.theme = theme
self.strings = strings
self.dateTimeFormat = dateTimeFormat
private var previousScreenBrightness: CGFloat?
private var displayLinkAnimator: DisplayLinkAnimator?
private let idleTimerExtensionDisposable: Disposable
public init(context: WalletContext, walletInfo: WalletInfo?, walletTransaction: WalletInfoTransaction, enableDebugActions: Bool) {
self.context = context
self.walletInfo = walletInfo
self.walletTransaction = walletTransaction
self.sectionId = sectionId
self.presentationData = context.presentationData
let defaultTheme = self.presentationData.theme.navigationBar
let navigationBarTheme = NavigationBarTheme(buttonColor: defaultTheme.buttonColor, disabledButtonColor: defaultTheme.disabledButtonColor, primaryTextColor: defaultTheme.primaryTextColor, backgroundColor: .clear, separatorColor: .clear, badgeBackgroundColor: defaultTheme.badgeBackgroundColor, badgeStrokeColor: defaultTheme.badgeStrokeColor, badgeTextColor: defaultTheme.badgeTextColor)
self.idleTimerExtensionDisposable = context.idleTimerExtension()
super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: navigationBarTheme, strings: NavigationBarStrings(back: self.presentationData.strings.Wallet_Navigation_Back, close: self.presentationData.strings.Wallet_Navigation_Close)))
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
self.navigationBar?.intrinsicCanTransitionInline = false
self.navigationPresentation = .flatModal
self.navigationItem.leftBarButtonItem = UIBarButtonItem(customView: UIView())
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Wallet_Navigation_Back, style: .plain, target: nil, action: nil)
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Wallet_Navigation_Done, style: .done, target: self, action: #selector(self.donePressed))
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (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() })
})
}
}
deinit {
self.idleTimerExtensionDisposable.dispose()
}
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()
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func loadDisplayNode() {
self.displayNode = WalletTransactionInfoScreenNode(context: self.context, presentationData: self.presentationData, walletTransaction: self.walletTransaction)
(self.displayNode as! WalletTransactionInfoScreenNode).send = { [weak self] address in
guard let strongSelf = self else {
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()
})
}
var randomId: Int64 = 0
arc4random_buf(&randomId, 8)
if let walletInfo = strongSelf.walletInfo {
strongSelf.push(walletSendScreen(context: strongSelf.context, randomId: randomId, walletInfo: walletInfo, address: address))
strongSelf.dismiss()
}
}
}
}
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: Int64
switch item.walletTransaction {
case let .completed(transaction):
transferredValue = transaction.transferredValueWithoutFees
case let .pending(transaction):
transferredValue = -transaction.value
(self.displayNode as! WalletTransactionInfoScreenNode).displayFeesTooltip = { [weak self] node, rect in
guard let strongSelf = self else {
return
}
if transferredValue <= 0 {
signString = ""
balanceString = "\(formatBalanceText(-transferredValue, decimalSeparator: item.dateTimeFormat.decimalSeparator))"
titleColor = item.theme.info.outgoingFundsTitleColor
} else {
signString = ""
balanceString = "\(formatBalanceText(transferredValue, decimalSeparator: item.dateTimeFormat.decimalSeparator))"
titleColor = item.theme.info.incomingFundsTitleColor
}
let title = NSMutableAttributedString()
if let range = balanceString.range(of: item.dateTimeFormat.decimalSeparator) {
let integralPart = String(balanceString[..<range.lowerBound])
let fractionalPart = String(balanceString[range.lowerBound...])
title.append(NSAttributedString(string: integralPart, font: Font.bold(48.0), textColor: titleColor))
title.append(NSAttributedString(string: fractionalPart, font: Font.bold(24.0), textColor: titleColor))
} else {
title.append(NSAttributedString(string: balanceString, font: Font.bold(48.0), textColor: titleColor))
}
let titleSign = NSAttributedString(string: signString, font: Font.bold(48.0), textColor: titleColor)
let timestamp: Int64
switch item.walletTransaction {
case let .completed(transaction):
timestamp = transaction.timestamp
case let .pending(transaction):
timestamp = transaction.timestamp
}
let subtitle: String = stringForFullDate(timestamp: Int32(clamping: timestamp), strings: item.strings, dateTimeFormat: item.dateTimeFormat)
let (titleSignLayout, titleSignApply) = makeTitleSignLayout(TextNodeLayoutArguments(attributedString: titleSign, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: title, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: subtitle, font: Font.regular(13.0), textColor: item.theme.list.freeTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let contentSize: CGSize
contentSize = CGSize(width: params.width, height: titleLayout.size.height + verticalInset + verticalInset)
let insets = itemListNeighborsGroupedInsets(neighbors)
let titleScale: CGFloat = min(1.0, (params.width - 40.0 - iconSize.width) / titleLayout.size.width)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
return (layout, { [weak self] in
var string = NSMutableAttributedString(string: "Blockchain validators collect a tiny fee for storing information about your decentralized wallet and for processing your transactions. More info", font: Font.regular(14.0), textColor: .white, paragraphAlignment: .center)
string.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor(rgb: 0x6bb2ff), range: NSMakeRange(string.string.count - 10, 10))
let controller = TooltipController(content: .attributedText(string), timeout: 3.0, dismissByTapOutside: true, dismissByTapOutsideSource: false, dismissImmediatelyOnLayoutUpdate: false)
strongSelf.present(controller, in: .window(.root), with: TooltipControllerPresentationArguments(sourceViewAndRect: {
if let strongSelf = self {
strongSelf.item = item
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height))
//strongSelf.activateArea.accessibilityLabel = attributedText.string
let _ = titleSignApply()
let _ = titleApply()
let _ = subtitleApply()
let iconSpacing: CGFloat = 4.0
let contentWidth = titleSignLayout.size.width + iconSpacing + titleLayout.size.width + iconSpacing + iconSize.width * 3.0 / 2.0
let contentOrigin = floor((params.width - contentWidth) / 2.0)
let titleSignFrame = CGRect(origin: CGPoint(x: contentOrigin, y: verticalInset), size: titleSignLayout.size)
let iconFrame = CGRect(origin: CGPoint(x: contentOrigin + titleSignFrame.width * titleScale + iconSpacing, y: titleSignFrame.minY + floor((titleLayout.size.height - iconSize.height) / 2.0) - 2.0), size: iconSize)
let titleFrame = CGRect(origin: CGPoint(x: iconFrame.maxX + iconSpacing, y: verticalInset), size: titleLayout.size)
let subtitleFrame = CGRect(origin: CGPoint(x: floor((params.width - subtitleLayout.size.width) / 2.0), y: titleFrame.maxY - 5.0), size: subtitleLayout.size)
strongSelf.titleSignNode.position = titleSignFrame.center
strongSelf.titleSignNode.bounds = CGRect(origin: CGPoint(), size: titleSignFrame.size)
strongSelf.titleSignNode.transform = CATransform3DMakeScale(titleScale, titleScale, 1.0)
strongSelf.titleNode.position = titleFrame.center
strongSelf.titleNode.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
strongSelf.titleNode.transform = CATransform3DMakeScale(titleScale, titleScale, 1.0)
strongSelf.subtitleNode.frame = subtitleFrame
strongSelf.iconNode.frame = iconFrame
return (node.view, rect.insetBy(dx: 0.0, dy: -4.0))
}
})
return nil
}))
}
self.displayNodeDidLoad()
}
private let measureTextNode = TextNode()
override func preferredContentSizeForLayout(_ layout: ContainerViewLayout) -> CGSize? {
let text = NSAttributedString(string: extractDescription(self.walletTransaction), font: Font.regular(17.0), textColor: .black)
let makeTextLayout = TextNode.asyncLayout(self.measureTextNode)
let (textLayout, _) = makeTextLayout(TextNodeLayoutArguments(attributedString: text, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: layout.size.width - 36.0 * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
var textHeight = textLayout.size.height
if textHeight > 0.0 {
textHeight += 24.0
}
let insets = layout.insets(options: [])
return CGSize(width: layout.size.width, height: 428.0 + insets.bottom + textHeight)
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
(self.displayNode as! WalletTransactionInfoScreenNode).containerLayoutUpdated(layout: layout, navigationHeight: self.navigationHeight, transition: transition)
}
@objc private func donePressed() {
self.dismiss()
}
}
private let integralFont = Font.medium(48.0)
private let fractionalFont = Font.medium(24.0)
private final class WalletTransactionInfoScreenNode: ViewControllerTracingNode {
private let context: WalletContext
private var presentationData: WalletPresentationData
private let walletTransaction: WalletInfoTransaction
private let incoming: Bool
private let titleNode: ImmediateTextNode
private let timeNode: ImmediateTextNode
private let amountNode: ImmediateTextNode
private let iconNode: AnimatedStickerNode
private let activateArea: AccessibilityAreaNode
private let feesNode: ImmediateTextNode
private let feesButtonNode: ASButtonNode
private let commentBackgroundNode: ASImageNode
private let commentTextNode: ImmediateTextNode
private let addressTextNode: ImmediateTextNode
private let buttonNode: SolidRoundedButtonNode
var send: ((String) -> Void)?
var displayFeesTooltip: ((ASDisplayNode, CGRect) -> Void)?
init(context: WalletContext, presentationData: WalletPresentationData, walletTransaction: WalletInfoTransaction) {
self.context = context
self.presentationData = presentationData
self.walletTransaction = walletTransaction
self.titleNode = ImmediateTextNode()
self.titleNode.textAlignment = .center
self.titleNode.maximumNumberOfLines = 1
self.timeNode = ImmediateTextNode()
self.timeNode.textAlignment = .center
self.timeNode.maximumNumberOfLines = 1
self.amountNode = ImmediateTextNode()
self.amountNode.textAlignment = .center
self.amountNode.maximumNumberOfLines = 1
self.iconNode = AnimatedStickerNode()
if let path = getAppBundle().path(forResource: "WalletIntroStatic", ofType: "tgs") {
self.iconNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 120, height: 120, mode: .direct)
self.iconNode.visibility = true
}
self.feesNode = ImmediateTextNode()
self.feesNode.textAlignment = .center
self.feesNode.maximumNumberOfLines = 2
self.feesNode.lineSpacing = 0.35
self.feesButtonNode = ASButtonNode()
self.commentBackgroundNode = ASImageNode()
self.commentBackgroundNode.contentMode = .scaleToFill
self.commentTextNode = ImmediateTextNode()
self.commentTextNode.textAlignment = .natural
self.commentTextNode.maximumNumberOfLines = 0
self.addressTextNode = ImmediateTextNode()
self.addressTextNode.maximumNumberOfLines = 4
self.addressTextNode.textAlignment = .justified
self.addressTextNode.lineSpacing = 0.35
self.buttonNode = SolidRoundedButtonNode(title: "", icon: nil, theme: SolidRoundedButtonTheme(backgroundColor: self.presentationData.theme.setup.buttonFillColor, foregroundColor: self.presentationData.theme.setup.buttonForegroundColor), height: 50.0, cornerRadius: 10.0, gloss: false)
self.activateArea = AccessibilityAreaNode()
let timestamp: Int64
let transferredValue: Int64
switch walletTransaction {
case let .completed(transaction):
timestamp = transaction.timestamp
transferredValue = transaction.transferredValueWithoutFees
case let .pending(transaction):
timestamp = transaction.timestamp
transferredValue = -transaction.value
}
self.incoming = transferredValue > 0
super.init()
self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
self.addSubnode(self.titleNode)
self.addSubnode(self.timeNode)
self.addSubnode(self.amountNode)
self.addSubnode(self.iconNode)
self.addSubnode(self.feesNode)
self.addSubnode(self.feesButtonNode)
self.addSubnode(self.commentBackgroundNode)
self.addSubnode(self.commentTextNode)
self.addSubnode(self.addressTextNode)
self.addSubnode(self.buttonNode)
let titleFont = Font.semibold(17.0)
let subtitleFont = Font.regular(13.0)
let addressFont = Font.monospace(17.0)
let textColor = self.presentationData.theme.list.itemPrimaryTextColor
let seccondaryTextColor = self.presentationData.theme.list.itemSecondaryTextColor
self.titleNode.attributedText = NSAttributedString(string: self.presentationData.strings.Wallet_TransactionInfo_Title, font: titleFont, textColor: textColor)
self.timeNode.attributedText = NSAttributedString(string: stringForFullDate(timestamp: Int32(clamping: timestamp), strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat), font: subtitleFont, textColor: seccondaryTextColor)
let amountString: String
let amountColor: UIColor
if transferredValue <= 0 {
amountString = "\(formatBalanceText(-transferredValue, decimalSeparator: self.presentationData.dateTimeFormat.decimalSeparator))"
amountColor = self.presentationData.theme.info.outgoingFundsTitleColor
} else {
amountString = "\(formatBalanceText(transferredValue, decimalSeparator: self.presentationData.dateTimeFormat.decimalSeparator))"
amountColor = self.presentationData.theme.info.incomingFundsTitleColor
}
self.amountNode.attributedText = amountAttributedString(amountString, integralFont: integralFont, fractionalFont: fractionalFont, color: amountColor)
var feesString: String = ""
if case let .completed(transaction) = walletTransaction {
if transaction.storageFee != 0 {
feesString.append(formatBalanceText(transaction.storageFee, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator) + " storage fee")
}
if transaction.otherFee != 0 {
if !feesString.isEmpty {
feesString.append("\n")
}
feesString.append(formatBalanceText(transaction.otherFee, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator) + " transaction fee")
}
if !feesString.isEmpty {
feesString.append("(?)")
}
}
self.feesNode.attributedText = NSAttributedString(string: feesString, font: subtitleFont, textColor: seccondaryTextColor)
self.feesButtonNode.addTarget(self, action: #selector(feesPressed), forControlEvents: .touchUpInside)
self.commentBackgroundNode.image = messageBubbleImage(incoming: transferredValue > 0, fillColor: UIColor(rgb: 0xf1f1f5), strokeColor: UIColor(rgb: 0xf1f1f5))
self.commentTextNode.attributedText = NSAttributedString(string: extractDescription(walletTransaction), font: Font.regular(17.0), textColor: .black)
let address = extractAddress(walletTransaction)
var singleAddress: String?
if case let .list(list) = address, list.count == 1 {
singleAddress = list.first
}
if let address = singleAddress {
self.addressTextNode.attributedText = NSAttributedString(string: formatAddress(address), font: addressFont, textColor: textColor, paragraphAlignment: .justified)
self.buttonNode.title = "Send Grams to This Address"
self.buttonNode.pressed = { [weak self] in
self?.send?(address)
}
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
@objc private func feesPressed() {
self.displayFeesTooltip?(self.feesNode, self.feesNode.bounds)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) {
var insets = layout.insets(options: [])
insets.top += navigationHeight
let inset: CGFloat = 22.0
let titleSize = self.titleNode.updateLayout(CGSize(width: layout.size.width - inset * 2.0, height: CGFloat.greatestFiniteMagnitude))
let titleFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - titleSize.width) / 2.0), y: 10.0), size: titleSize)
transition.updateFrame(node: self.titleNode, frame: titleFrame)
let subtitleSize = self.timeNode.updateLayout(CGSize(width: layout.size.width - inset * 2.0, height: CGFloat.greatestFiniteMagnitude))
let subtitleFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - subtitleSize.width) / 2.0), y: titleFrame.maxY + 1.0), size: subtitleSize)
transition.updateFrame(node: self.timeNode, frame: subtitleFrame)
let amountSize = self.amountNode.updateLayout(CGSize(width: layout.size.width - inset * 2.0, height: CGFloat.greatestFiniteMagnitude))
let amountFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - amountSize.width) / 2.0) + 18.0, y: 90.0), size: amountSize)
transition.updateFrame(node: self.amountNode, frame: amountFrame)
let iconSize = CGSize(width: 50.0, height: 50.0)
let iconFrame = CGRect(origin: CGPoint(x: amountFrame.minX - iconSize.width, y: amountFrame.minY), size: iconSize)
self.iconNode.updateLayout(size: iconFrame.size)
self.iconNode.frame = iconFrame
let feesSize = self.feesNode.updateLayout(CGSize(width: layout.size.width - inset * 2.0, height: CGFloat.greatestFiniteMagnitude))
let feesFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - feesSize.width) / 2.0), y: amountFrame.maxY + 8.0), size: feesSize)
transition.updateFrame(node: self.feesNode, frame: feesFrame)
transition.updateFrame(node: self.feesButtonNode, frame: feesFrame)
let commentSize = self.commentTextNode.updateLayout(CGSize(width: layout.size.width - 36.0 * 2.0, height: CGFloat.greatestFiniteMagnitude))
let commentFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - commentSize.width) / 2.0), y: amountFrame.maxY + 84.0), size: CGSize(width: commentSize.width, height: commentSize.height))
transition.updateFrame(node: self.commentTextNode, frame: commentFrame)
var commentBackgroundFrame = commentSize.width > 0.0 ? commentFrame.insetBy(dx: -11.0, dy: -7.0) : CGRect()
commentBackgroundFrame.size.width += 7.0
if self.incoming {
commentBackgroundFrame.origin.x -= 7.0
}
transition.updateFrame(node: self.commentBackgroundNode, frame: commentBackgroundFrame)
let buttonSideInset: CGFloat = 16.0
let bottomInset = insets.bottom + 10.0
let buttonWidth = layout.size.width - buttonSideInset * 2.0
let buttonHeight: CGFloat = 50.0
let buttonFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - buttonWidth) / 2.0), y: layout.size.height - bottomInset - buttonHeight), size: CGSize(width: buttonWidth, height: buttonHeight))
transition.updateFrame(node: self.buttonNode, frame: buttonFrame)
self.buttonNode.updateLayout(width: buttonFrame.width, transition: transition)
let addressSize = self.addressTextNode.updateLayout(CGSize(width: layout.size.width - inset * 2.0, height: CGFloat.greatestFiniteMagnitude))
transition.updateFrame(node: self.addressTextNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - addressSize.width) / 2.0), y: buttonFrame.minY - addressSize.height - 44.0), size: addressSize))
}
}

View File

@ -1,4 +1,5 @@
import Foundation
import UIKit
import UrlEscaping
let walletAddressLength: Int = 48
@ -186,3 +187,17 @@ func walletInvoiceUrl(address: String, amount: String? = nil, comment: String? =
}
return "ton://transfer/\(address)\(arguments)"
}
private let amountDelimeterCharacters = CharacterSet(charactersIn: "0123456789").inverted
func amountAttributedString(_ string: String, integralFont: UIFont, fractionalFont: UIFont, color: UIColor) -> NSAttributedString {
let result = NSMutableAttributedString()
if let range = string.rangeOfCharacter(from: amountDelimeterCharacters) {
let integralPart = String(string[..<range.lowerBound])
let fractionalPart = String(string[range.lowerBound...])
result.append(NSAttributedString(string: integralPart, font: integralFont, textColor: color))
result.append(NSAttributedString(string: fractionalPart, font: fractionalFont, textColor: color))
} else {
result.append(NSAttributedString(string: string, font: integralFont, textColor: color))
}
return result
}