mirror of
https://github.com/status-im/status-desktop.git
synced 2025-01-10 14:26:34 +00:00
feat(wallet2): introduce saved addresses
Closes #3307. WalletV2 view can be toggled between normal wallet view and the SavedAddresses view. Users can load, add, edit, and delete saved addresses. Favouriting a saved address is out of scope, as is sending to a saved addresses, drilling down in to a saved address, and supporting multiple networks. Updates components that utilised the StatusMinLengthValidator component to support the changes made to StatusQ. ### Notes 1. Depends on status-go PR https://github.com/status-im/status-go/pull/2356 2. Depends on StatusQ PR https://github.com/status-im/StatusQ/pull/394. # Conflicts: # src/app/wallet/v2/view.nim # ui/app/AppLayouts/WalletV2/WalletV2Layout.qml
This commit is contained in:
parent
e6f8a79f67
commit
9854a49a44
@ -1,10 +1,13 @@
|
||||
import atomics, strformat, strutils, sequtils, json, std/wrapnils, parseUtils, tables
|
||||
import NimQml, chronicles, stint
|
||||
import
|
||||
std/[atomics, json, parseutils, sequtils, strformat, strutils, tables, wrapnils]
|
||||
|
||||
import status/[status, wallet2]
|
||||
import views/[accounts, account_list, collectibles, settings, networks]
|
||||
import views/buy_sell_crypto/[service_controller]
|
||||
import ../../../app_service/[main]
|
||||
import
|
||||
chronicles, nimqml, status/[status, wallet2], stint
|
||||
|
||||
import
|
||||
../../../app_service/[main],
|
||||
./views/[accounts, account_list, collectibles, networks, saved_addresses, settings],
|
||||
./views/buy_sell_crypto/[service_controller]
|
||||
|
||||
QtObject:
|
||||
type
|
||||
@ -16,11 +19,13 @@ QtObject:
|
||||
settingsView*: SettingsView
|
||||
networksView*: NetworksView
|
||||
cryptoServiceController: CryptoServiceController
|
||||
savedAddressesView: SavedAddressesView
|
||||
|
||||
proc delete(self: WalletView) =
|
||||
self.accountsView.delete
|
||||
self.collectiblesView.delete
|
||||
self.cryptoServiceController.delete
|
||||
self.savedAddressesView.delete
|
||||
self.QAbstractListModel.delete
|
||||
self.settingsView.delete
|
||||
self.networksView.delete
|
||||
@ -37,6 +42,7 @@ QtObject:
|
||||
result.settingsView = newSettingsView()
|
||||
result.networksView = newNetworksView()
|
||||
result.cryptoServiceController = newCryptoServiceController(status, appService)
|
||||
result.savedAddressesView = newSavedAddressesView(status, appService)
|
||||
result.setup
|
||||
|
||||
proc getAccounts(self: WalletView): QVariant {.slot.} =
|
||||
@ -44,7 +50,7 @@ QtObject:
|
||||
QtProperty[QVariant] accountsView:
|
||||
read = getAccounts
|
||||
|
||||
proc getCollectibles(self: WalletView): QVariant {.slot.} =
|
||||
proc getCollectibles(self: WalletView): QVariant {.slot.} =
|
||||
return newQVariant(self.collectiblesView)
|
||||
QtProperty[QVariant] collectiblesView:
|
||||
read = getCollectibles
|
||||
@ -57,6 +63,10 @@ QtObject:
|
||||
QtProperty[QVariant] networksView:
|
||||
read = getNetworks
|
||||
|
||||
proc getSavedAddressesView(self: WalletView): QVariant {.slot.} = newQVariant(self.savedAddressesView)
|
||||
QtProperty[QVariant] savedAddressesView:
|
||||
read = getSavedAddressesView
|
||||
|
||||
proc updateView*(self: WalletView) =
|
||||
# TODO:
|
||||
self.accountsView.triggerUpdateAccounts()
|
||||
|
215
src/app/wallet/v2/views/saved_addresses.nim
Normal file
215
src/app/wallet/v2/views/saved_addresses.nim
Normal file
@ -0,0 +1,215 @@
|
||||
import # std libs
|
||||
std/json
|
||||
|
||||
import # vendor libs
|
||||
chronicles, nimqml,
|
||||
status/status, status/types/conversions, status/wallet2/saved_addresses,
|
||||
stew/results
|
||||
|
||||
import # status-desktop modules
|
||||
../../../../app_service/main, ../../../../app_service/tasks/[qt, threadpool],
|
||||
./saved_addresses_list
|
||||
|
||||
logScope:
|
||||
topics = "saved-addresses-view"
|
||||
|
||||
type
|
||||
AddSavedAddressTaskArg = ref object of QObjectTaskArg
|
||||
savedAddress: SavedAddress
|
||||
|
||||
DeleteSavedAddressTaskArg = ref object of QObjectTaskArg
|
||||
address: Address
|
||||
|
||||
EditSavedAddressTaskArg = ref object of QObjectTaskArg
|
||||
savedAddress: SavedAddress
|
||||
|
||||
LoadSavedAddressesTaskArg = ref object of QObjectTaskArg
|
||||
|
||||
const loadSavedAddressesTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.} =
|
||||
let
|
||||
arg = decode[LoadSavedAddressesTaskArg](argEncoded)
|
||||
output = saved_addresses.getSavedAddresses()
|
||||
arg.finish(output)
|
||||
|
||||
proc loadSavedAddresses[T](self: T, slot: string) =
|
||||
let arg = LoadSavedAddressesTaskArg(
|
||||
tptr: cast[ByteAddress](loadSavedAddressesTask),
|
||||
vptr: cast[ByteAddress](self.vptr),
|
||||
slot: slot
|
||||
)
|
||||
self.appService.threadpool.start(arg)
|
||||
|
||||
const addSavedAddressTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.} =
|
||||
let
|
||||
arg = decode[AddSavedAddressTaskArg](argEncoded)
|
||||
output = saved_addresses.addSavedAddress(arg.savedAddress)
|
||||
arg.finish(output)
|
||||
|
||||
proc addSavedAddress[T](self: T, slot, name, address: string) =
|
||||
let arg = AddSavedAddressTaskArg(
|
||||
tptr: cast[ByteAddress](addSavedAddressTask),
|
||||
vptr: cast[ByteAddress](self.vptr),
|
||||
slot: slot
|
||||
)
|
||||
var addressParsed: Address
|
||||
try:
|
||||
addressParsed = Address.fromHex(address)
|
||||
except:
|
||||
raise newException(ValueError, "Error parsing address")
|
||||
arg.savedAddress = SavedAddress(name: name, address: addressParsed)
|
||||
self.appService.threadpool.start(arg)
|
||||
|
||||
const deleteSavedAddressTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.} =
|
||||
let
|
||||
arg = decode[DeleteSavedAddressTaskArg](argEncoded)
|
||||
output = saved_addresses.deleteSavedAddress(arg.address)
|
||||
arg.finish(output)
|
||||
|
||||
proc deleteSavedAddress[T](self: T, slot, address: string) =
|
||||
let arg = DeleteSavedAddressTaskArg(
|
||||
tptr: cast[ByteAddress](deleteSavedAddressTask),
|
||||
vptr: cast[ByteAddress](self.vptr),
|
||||
slot: slot
|
||||
)
|
||||
var addressParsed: Address
|
||||
try:
|
||||
addressParsed = Address.fromHex(address)
|
||||
except:
|
||||
raise newException(ValueError, "Error parsing address")
|
||||
arg.address = addressParsed
|
||||
self.appService.threadpool.start(arg)
|
||||
|
||||
const editSavedAddressTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.} =
|
||||
let
|
||||
arg = decode[EditSavedAddressTaskArg](argEncoded)
|
||||
output = saved_addresses.editSavedAddress(arg.savedAddress)
|
||||
arg.finish(output)
|
||||
|
||||
proc editSavedAddress[T](self: T, slot, name, address: string) =
|
||||
let arg = EditSavedAddressTaskArg(
|
||||
tptr: cast[ByteAddress](editSavedAddressTask),
|
||||
vptr: cast[ByteAddress](self.vptr),
|
||||
slot: slot
|
||||
)
|
||||
var addressParsed: Address
|
||||
try:
|
||||
addressParsed = Address.fromHex(address)
|
||||
except:
|
||||
raise newException(ValueError, "Error parsing address")
|
||||
arg.savedAddress = SavedAddress(name: name, address: addressParsed)
|
||||
self.appService.threadpool.start(arg)
|
||||
|
||||
QtObject:
|
||||
type
|
||||
SavedAddressesView* = ref object of QObject
|
||||
# no need to store the seq[SavedAddress] value in `loadResult`, as it is
|
||||
# set in self.savedAddresses
|
||||
appService: AppService
|
||||
addEditResult: SavedAddressResult[void]
|
||||
deleteResult: SavedAddressResult[void]
|
||||
loadResult: SavedAddressResult[void]
|
||||
savedAddresses: SavedAddressesList
|
||||
status: Status
|
||||
|
||||
proc setup(self: SavedAddressesView) = self.QObject.setup
|
||||
|
||||
proc delete(self: SavedAddressesView) =
|
||||
self.savedAddresses.delete
|
||||
self.QObject.delete
|
||||
|
||||
proc newSavedAddressesView*(status: Status, appService: AppService): SavedAddressesView =
|
||||
new(result, delete)
|
||||
result.addEditResult = SavedAddressResult[void].ok()
|
||||
result.appService = appService
|
||||
result.deleteResult = SavedAddressResult[void].ok()
|
||||
result.savedAddresses = newSavedAddressesList()
|
||||
result.setup
|
||||
result.status = status
|
||||
|
||||
# START QtProperty notify backing signals
|
||||
proc addEditResultChanged*(self: SavedAddressesView) {.signal.}
|
||||
proc deleteResultChanged*(self: SavedAddressesView) {.signal.}
|
||||
proc loadResultChanged*(self: SavedAddressesView) {.signal.}
|
||||
proc savedAddressesChanged*(self: SavedAddressesView) {.signal.}
|
||||
# END QtProperty notify backing signals
|
||||
|
||||
# START QtProperty get backing procs
|
||||
proc getAddEditResult(self: SavedAddressesView): string {.slot.} =
|
||||
return Json.encode(self.addEditResult)
|
||||
|
||||
proc getDeleteResult(self: SavedAddressesView): string {.slot.} =
|
||||
return Json.encode(self.deleteResult)
|
||||
|
||||
proc getLoadResult(self: SavedAddressesView): string {.slot.} =
|
||||
return Json.encode(self.loadResult)
|
||||
|
||||
proc getSavedAddressesList(self: SavedAddressesView): QVariant {.slot.} =
|
||||
return newQVariant(self.savedAddresses)
|
||||
# END QtProperty get backing procs
|
||||
|
||||
# START QtProperties
|
||||
QtProperty[string] addEditResult:
|
||||
read = getAddEditResult
|
||||
notify = addEditResultChanged
|
||||
|
||||
QtProperty[string] deleteResult:
|
||||
read = getDeleteResult
|
||||
notify = deleteResultChanged
|
||||
|
||||
QtProperty[string] loadResult:
|
||||
read = getLoadResult
|
||||
notify = loadResultChanged
|
||||
|
||||
QtProperty[QVariant] savedAddresses:
|
||||
read = getSavedAddressesList
|
||||
notify = savedAddressesChanged
|
||||
# END QtProperties
|
||||
|
||||
# START Task runner callbacks
|
||||
proc setSavedAddressesList(self: SavedAddressesView, raw: string) {.slot.} =
|
||||
let savedAddressesResult = Json.decode(raw, SavedAddressResult[seq[SavedAddress]])
|
||||
|
||||
if savedAddressesResult.isOk:
|
||||
self.savedAddresses.setData(savedAddressesResult.get)
|
||||
self.savedAddressesChanged()
|
||||
self.loadResult = SavedAddressResult[void].ok()
|
||||
else:
|
||||
self.loadResult = SavedAddressResult[void].err(savedAddressesResult.error)
|
||||
self.loadResultChanged()
|
||||
|
||||
proc afterAddEdit(self: SavedAddressesView, raw: string) {.slot.} =
|
||||
let addEditResult = Json.decode(raw, SavedAddressResult[void])
|
||||
self.addEditResult = addEditResult
|
||||
self.addEditResultChanged()
|
||||
|
||||
proc afterDelete(self: SavedAddressesView, raw: string) {.slot.} =
|
||||
let deleteResult = Json.decode(raw, SavedAddressResult[void])
|
||||
self.deleteResult = deleteResult
|
||||
self.deleteResultChanged()
|
||||
# END Task runner callbacks
|
||||
|
||||
# START slots
|
||||
proc loadSavedAddresses*(self: SavedAddressesView) {.slot.} =
|
||||
self.loadSavedAddresses("setSavedAddressesList")
|
||||
|
||||
proc addSavedAddress*(self: SavedAddressesView, name: string, address: string) {.slot.} =
|
||||
try:
|
||||
self.addSavedAddress("afterAddEdit", name, address)
|
||||
except ValueError as e:
|
||||
self.addEditResult = SavedAddressResult[void].err(ParseAddressError)
|
||||
self.addEditResultChanged()
|
||||
|
||||
proc deleteSavedAddress*(self: SavedAddressesView, address: string) {.slot.} =
|
||||
try:
|
||||
self.deleteSavedAddress("afterDelete", address)
|
||||
except ValueError as e:
|
||||
self.deleteResult = SavedAddressResult[void].err(ParseAddressError)
|
||||
self.deleteResultChanged()
|
||||
|
||||
proc editSavedAddress*(self: SavedAddressesView, name: string, address: string) {.slot.} =
|
||||
try:
|
||||
self.editSavedAddress("afterAddEdit", name, address)
|
||||
except ValueError as e:
|
||||
self.addEditResult = SavedAddressResult[void].err(ParseAddressError)
|
||||
self.addEditResultChanged()
|
||||
# END slots
|
62
src/app/wallet/v2/views/saved_addresses_list.nim
Normal file
62
src/app/wallet/v2/views/saved_addresses_list.nim
Normal file
@ -0,0 +1,62 @@
|
||||
import # std libs
|
||||
std/tables
|
||||
|
||||
import # vendor libs
|
||||
nimqml, status/types/address
|
||||
|
||||
type
|
||||
SavedAddressRoles {.pure.} = enum
|
||||
Name = UserRole + 1,
|
||||
Address = UserRole + 2
|
||||
|
||||
QtObject:
|
||||
type SavedAddressesList* = ref object of QAbstractListModel
|
||||
savedAddresses*: seq[SavedAddress]
|
||||
|
||||
proc setup(self: SavedAddressesList) = self.QAbstractListModel.setup
|
||||
|
||||
proc delete(self: SavedAddressesList) =
|
||||
self.savedAddresses = @[]
|
||||
self.QAbstractListModel.delete
|
||||
|
||||
proc newSavedAddressesList*(): SavedAddressesList =
|
||||
new(result, delete)
|
||||
result.savedAddresses = @[]
|
||||
result.setup
|
||||
|
||||
proc getSavedAddress*(self: SavedAddressesList, index: int): SavedAddress =
|
||||
self.savedAddresses[index]
|
||||
|
||||
proc rowData(self: SavedAddressesList, index: int, column: string): string {.slot.} =
|
||||
if (index >= self.savedAddresses.len):
|
||||
return
|
||||
|
||||
let savedAddress = self.savedAddresses[index]
|
||||
case column:
|
||||
of "name": result = savedAddress.name
|
||||
of "address": result = $savedAddress.address
|
||||
|
||||
method rowCount*(self: SavedAddressesList, index: QModelIndex = nil): int =
|
||||
return self.savedAddresses.len
|
||||
|
||||
method data(self: SavedAddressesList, index: QModelIndex, role: int): QVariant =
|
||||
if not index.isValid:
|
||||
return
|
||||
|
||||
if index.row < 0 or index.row >= self.savedAddresses.len:
|
||||
return
|
||||
|
||||
let savedAddress = self.savedAddresses[index.row]
|
||||
let collectionRole = role.SavedAddressRoles
|
||||
case collectionRole:
|
||||
of SavedAddressRoles.Name: result = newQVariant(savedAddress.name)
|
||||
of SavedAddressRoles.Address: result = newQVariant($savedAddress.address)
|
||||
|
||||
method roleNames(self: SavedAddressesList): Table[int, string] =
|
||||
{ SavedAddressRoles.Name.int:"name",
|
||||
SavedAddressRoles.Address.int:"address"}.toTable
|
||||
|
||||
proc setData*(self: SavedAddressesList, savedAddresses: seq[SavedAddress]) =
|
||||
self.beginResetModel()
|
||||
self.savedAddresses = savedAddresses
|
||||
self.endResetModel()
|
@ -53,8 +53,10 @@ StatusModal {
|
||||
id: nameInput
|
||||
charLimit: maxCategoryNameLength
|
||||
input.placeholderText: qsTr("Category title")
|
||||
validators: [StatusMinLengthValidator { minLength: 1 }]
|
||||
onTextChanged: errorMessage = Utils.getErrorMessage(errors, "category name")
|
||||
validators: [StatusMinLengthValidator {
|
||||
minLength: 1
|
||||
errorMessage: Utils.getErrorMessage(errors, qsTr("category name"))
|
||||
}]
|
||||
}
|
||||
|
||||
StatusModalDivider {
|
||||
|
@ -78,9 +78,11 @@ StatusModal {
|
||||
input.onTextChanged: {
|
||||
input.text = Utils.convertSpacesToDashesAndUpperToLowerCase(input.text);
|
||||
input.cursorPosition = input.text.length
|
||||
errorMessage = Utils.getErrorMessage(errors, qsTr("channel name"))
|
||||
}
|
||||
validators: [StatusMinLengthValidator { minLength: 1 }]
|
||||
validators: [StatusMinLengthValidator {
|
||||
minLength: 1
|
||||
errorMessage: Utils.getErrorMessage(errors, qsTr("channel name"))
|
||||
}]
|
||||
}
|
||||
|
||||
StatusModalDivider {
|
||||
@ -96,8 +98,10 @@ StatusModal {
|
||||
input.placeholderText: qsTr("Describe the channel")
|
||||
input.multiline: true
|
||||
input.implicitHeight: 88
|
||||
input.onTextChanged: errorMessage = Utils.getErrorMessage(errors, qsTr("channel description"))
|
||||
validators: [StatusMinLengthValidator { minLength: 1 }]
|
||||
validators: [StatusMinLengthValidator {
|
||||
minLength: 1
|
||||
errorMessage: Utils.getErrorMessage(errors, qsTr("channel description"))
|
||||
}]
|
||||
}
|
||||
|
||||
/* TODO: use the code below to enable private channels and message limit */
|
||||
|
@ -89,8 +89,10 @@ StatusModal {
|
||||
id: nameInput
|
||||
charLimit: maxCommunityNameLength
|
||||
input.placeholderText: qsTr("A catchy name")
|
||||
validators: [StatusMinLengthValidator { minLength: 1 }]
|
||||
onTextChanged: errorMessage = Utils.getErrorMessage(errors, "community name")
|
||||
validators: [StatusMinLengthValidator {
|
||||
minLength: 1
|
||||
errorMessage: Utils.getErrorMessage(errors, "community name")
|
||||
}]
|
||||
}
|
||||
|
||||
StatusInput {
|
||||
@ -102,8 +104,10 @@ StatusModal {
|
||||
input.multiline: true
|
||||
input.implicitHeight: 88
|
||||
|
||||
validators: [StatusMinLengthValidator { minLength: 1 }]
|
||||
onTextChanged: errorMessage = Utils.getErrorMessage(errors, "community description")
|
||||
validators: [StatusMinLengthValidator {
|
||||
minLength: 1
|
||||
errorMessage: Utils.getErrorMessage(errors, "community description")
|
||||
}]
|
||||
}
|
||||
|
||||
StatusBaseText {
|
||||
|
@ -2,6 +2,7 @@ import QtQuick 2.13
|
||||
import QtQuick.Controls 2.13
|
||||
import QtQuick.Layouts 1.13
|
||||
import QtGraphicalEffects 1.13
|
||||
import StatusQ.Components 0.1
|
||||
import StatusQ.Core 0.1
|
||||
import StatusQ.Core.Theme 0.1
|
||||
import "../../../imports"
|
||||
@ -152,7 +153,7 @@ Rectangle {
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onEntered: {
|
||||
rectangle.hovered = true
|
||||
rectangle.hovered = true
|
||||
}
|
||||
onExited: {
|
||||
rectangle.hovered = false
|
||||
@ -168,7 +169,7 @@ Rectangle {
|
||||
id: accountsList
|
||||
anchors.right: parent.right
|
||||
anchors.left: parent.left
|
||||
height: (listView.count <= 8) ? (listView.count * 64) : 530
|
||||
height: (listView.count <= 8) ? ((listView.count * 64) + (Style.current.padding * 2)) : 530
|
||||
anchors.top: walletValueTextContainer.bottom
|
||||
anchors.topMargin: Style.current.padding
|
||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||
@ -176,6 +177,7 @@ Rectangle {
|
||||
|
||||
ListView {
|
||||
id: listView
|
||||
clip: true
|
||||
anchors.fill: parent
|
||||
spacing: 5
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
@ -203,6 +205,63 @@ Rectangle {
|
||||
balance: "12.00 USD"
|
||||
iconColor: "#7CDA00"
|
||||
}
|
||||
ListElement {
|
||||
name: "Status account"
|
||||
address: "0xcfc9f08bbcbcb80760e8cb9a3c1232d19662fc6f"
|
||||
balance: "12.00 USD"
|
||||
iconColor: "#7CDA00"
|
||||
}
|
||||
|
||||
ListElement {
|
||||
name: "Test account 1"
|
||||
address: "0x2Ef1...E0Ba"
|
||||
balance: "12.00 USD"
|
||||
iconColor: "#FA6565"
|
||||
}
|
||||
ListElement {
|
||||
name: "Status account"
|
||||
address: "0x2Ef1...E0Ba"
|
||||
balance: "12.00 USD"
|
||||
iconColor: "#7CDA00"
|
||||
}
|
||||
ListElement {
|
||||
name: "Status account"
|
||||
address: "0xcfc9f08bbcbcb80760e8cb9a3c1232d19662fc6f"
|
||||
balance: "12.00 USD"
|
||||
iconColor: "#7CDA00"
|
||||
}
|
||||
|
||||
ListElement {
|
||||
name: "Test account 1"
|
||||
address: "0x2Ef1...E0Ba"
|
||||
balance: "12.00 USD"
|
||||
iconColor: "#FA6565"
|
||||
}
|
||||
ListElement {
|
||||
name: "Status account"
|
||||
address: "0x2Ef1...E0Ba"
|
||||
balance: "12.00 USD"
|
||||
iconColor: "#7CDA00"
|
||||
}
|
||||
ListElement {
|
||||
name: "Status account"
|
||||
address: "0xcfc9f08bbcbcb80760e8cb9a3c1232d19662fc6f"
|
||||
balance: "12.00 USD"
|
||||
iconColor: "#7CDA00"
|
||||
}
|
||||
|
||||
ListElement {
|
||||
name: "Test account 1"
|
||||
address: "0x2Ef1...E0Ba"
|
||||
balance: "12.00 USD"
|
||||
iconColor: "#FA6565"
|
||||
}
|
||||
ListElement {
|
||||
name: "Status account 12"
|
||||
address: "0x2Ef1...E0Ba"
|
||||
balance: "12.00 USD"
|
||||
iconColor: "#7CDA00"
|
||||
}
|
||||
}
|
||||
|
||||
model: walletV2Model.accountsView.accounts
|
||||
@ -216,23 +275,20 @@ Rectangle {
|
||||
anchors.top: accountsList.bottom
|
||||
anchors.topMargin: 31
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: savedAdressesLabel
|
||||
height: 20
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Style.current.padding
|
||||
StatusNavigationListItem {
|
||||
id: btnSavedAddresses
|
||||
title: qsTr("Saved addresses")
|
||||
icon.name: "address"
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 22
|
||||
StatusIcon {
|
||||
color: Theme.palette.baseColor1
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
icon: "address"
|
||||
}
|
||||
StatusBaseText {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
text: qsTr("Saved addresses")
|
||||
color: Theme.palette.baseColor1
|
||||
anchors.left: parent.left
|
||||
anchors.bottomMargin: Style.current.halfPadding
|
||||
anchors.leftMargin: Style.current.smallPadding
|
||||
|
||||
onClicked: {
|
||||
selected = !selected;
|
||||
selected ?
|
||||
walletView.showSavedAddressesView() :
|
||||
walletView.hideSavedAddressesView();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
346
ui/app/AppLayouts/WalletV2/SavedAddresses.qml
Normal file
346
ui/app/AppLayouts/WalletV2/SavedAddresses.qml
Normal file
@ -0,0 +1,346 @@
|
||||
import QtQuick 2.13
|
||||
import QtQuick.Controls 2.13
|
||||
import QtQuick.Layouts 1.13
|
||||
|
||||
import "../../../imports"
|
||||
import "../../../shared"
|
||||
import "./components"
|
||||
|
||||
import StatusQ.Controls 0.1
|
||||
import StatusQ.Components 0.1
|
||||
import StatusQ.Core 0.1
|
||||
import StatusQ.Core.Theme 0.1
|
||||
import StatusQ.Popups 0.1
|
||||
|
||||
Item {
|
||||
id: root
|
||||
property bool loading: false
|
||||
property int error: SavedAddresses.Error.None
|
||||
anchors.leftMargin: 80
|
||||
anchors.rightMargin: 80
|
||||
anchors.topMargin: 62
|
||||
|
||||
|
||||
enum Error {
|
||||
CreateSavedAddressError,
|
||||
DeleteSavedAddressError,
|
||||
ParseAddressError,
|
||||
ReadSavedAddressesError,
|
||||
UpdateSavedAddressError,
|
||||
None
|
||||
}
|
||||
|
||||
function getErrorText(error) {
|
||||
switch (error) {
|
||||
case SavedAddresses.Error.CreateSavedAddressError:
|
||||
return qsTr("Error creating new saved address, please try again later.");
|
||||
case SavedAddresses.Error.DeleteSavedAddressError:
|
||||
return qsTr("Error deleting saved address, please try again later.");
|
||||
case SavedAddresses.Error.ReadSavedAddressesError:
|
||||
return qsTr("Error getting saved addresses, please try again later.");
|
||||
case SavedAddresses.Error.UpdateSavedAddressError:
|
||||
return qsTr("Error updating saved address, please try again later.");
|
||||
default: return "";
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: header
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
height: btnAdd.height
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.right: btnAdd.left
|
||||
spacing: 10
|
||||
|
||||
StatusIcon {
|
||||
icon: "address"
|
||||
color: Theme.palette.primaryColor1
|
||||
width: undefined
|
||||
height: 35
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
StatusBaseText {
|
||||
id: title
|
||||
text: qsTr("Saved addresses")
|
||||
font.weight: Font.Medium
|
||||
font.pixelSize: 28
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: Theme.palette.directColor1
|
||||
}
|
||||
}
|
||||
Component {
|
||||
id: addEditSavedAddress
|
||||
AddEditSavedAddress {
|
||||
id: addEditModal
|
||||
anchors.centerIn: parent
|
||||
onClosed: {
|
||||
destroy()
|
||||
}
|
||||
onBeforeSave: function() {
|
||||
root.loading = true
|
||||
}
|
||||
}
|
||||
}
|
||||
StatusButton {
|
||||
id: btnAdd
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
text: "Add new +"
|
||||
leftPadding: 8
|
||||
rightPadding: 11
|
||||
visible: !root.loading
|
||||
onClicked: {
|
||||
appMain.openPopup(addEditSavedAddress)
|
||||
}
|
||||
}
|
||||
StatusLoadingIndicator {
|
||||
visible: root.loading
|
||||
color: Theme.palette.directColor4
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: delegateSavedAddress
|
||||
StatusListItem {
|
||||
id: savedAddress
|
||||
title: name
|
||||
subTitle: address
|
||||
icon.name: "wallet"
|
||||
implicitWidth: parent.width
|
||||
property bool showButtons: sensor.containsMouse
|
||||
components: [
|
||||
StatusRoundButton {
|
||||
color: hovered ? Theme.palette.dangerColor2 : Theme.palette.dangerColor3
|
||||
icon.color: Theme.palette.dangerColor1
|
||||
visible: showButtons
|
||||
icon.name: "delete"
|
||||
onClicked: {
|
||||
deleteAddressConfirm.name = name
|
||||
deleteAddressConfirm.address = address
|
||||
deleteAddressConfirm.open()
|
||||
}
|
||||
},
|
||||
StatusRoundButton {
|
||||
icon.name: "pencil"
|
||||
visible: showButtons
|
||||
onClicked: appMain.openPopup(addEditSavedAddress,
|
||||
{
|
||||
edit: true,
|
||||
address: address,
|
||||
name: name
|
||||
})
|
||||
},
|
||||
StatusRoundButton {
|
||||
icon.name: "send"
|
||||
visible: showButtons
|
||||
},
|
||||
StatusRoundButton {
|
||||
color: hovered ? Theme.palette.pinColor2 : Theme.palette.pinColor3
|
||||
icon.color: Theme.palette.pinColor1
|
||||
icon.name: "favourite"
|
||||
visible: showButtons
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
StatusModal {
|
||||
id: deleteAddressConfirm
|
||||
property string address
|
||||
property string name
|
||||
// NOTE: the `text` property was created as a workaround because
|
||||
// setting StatusBaseText.text to `qsTr("...").arg("...")`
|
||||
// caused no text to render
|
||||
property string text: qsTr("Are you sure you want to remove '%1' from your saved addresses?").arg(name)
|
||||
anchors.centerIn: parent
|
||||
header.title: "Are you sure?"
|
||||
header.subTitle: name
|
||||
contentItem: StatusBaseText {
|
||||
anchors.centerIn: parent
|
||||
height: contentHeight + topPadding + bottomPadding
|
||||
text: deleteAddressConfirm.text
|
||||
font.pixelSize: 15
|
||||
color: Theme.palette.directColor1
|
||||
wrapMode: Text.Wrap
|
||||
topPadding: Style.current.padding
|
||||
rightPadding: Style.current.padding
|
||||
bottomPadding: Style.current.padding
|
||||
leftPadding: Style.current.padding
|
||||
}
|
||||
rightButtons: [
|
||||
StatusButton {
|
||||
text: qsTr("Cancel")
|
||||
onClicked: deleteAddressConfirm.close()
|
||||
},
|
||||
StatusButton {
|
||||
type: StatusBaseButton.Type.Danger
|
||||
text: qsTr("Delete")
|
||||
onClicked: {
|
||||
root.loading = true
|
||||
walletV2Model.savedAddressesView.deleteSavedAddress(
|
||||
deleteAddressConfirm.address)
|
||||
deleteAddressConfirm.close()
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
}
|
||||
Connections {
|
||||
target: walletV2Model.savedAddressesView
|
||||
onAddEditResultChanged: {
|
||||
root.loading = false
|
||||
let resultRaw = walletV2Model.savedAddressesView.addEditResult
|
||||
let result = JSON.parse(resultRaw)
|
||||
if (result.o) {
|
||||
root.error = SavedAddresses.Error.None
|
||||
walletV2Model.savedAddressesView.loadSavedAddresses();
|
||||
} else {
|
||||
root.error = parseInt(result.e)
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: walletV2Model.savedAddressesView
|
||||
onDeleteResultChanged: {
|
||||
root.loading = false
|
||||
let resultRaw = walletV2Model.savedAddressesView.deleteResult
|
||||
let result = JSON.parse(resultRaw)
|
||||
if (result.o) {
|
||||
root.error = SavedAddresses.Error.None
|
||||
walletV2Model.savedAddressesView.loadSavedAddresses();
|
||||
deleteAddressConfirm.close();
|
||||
} else {
|
||||
root.error = parseInt(result.e)
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: walletV2Model.savedAddressesView
|
||||
onLoadResultChanged: {
|
||||
root.loading = false
|
||||
let resultRaw = walletV2Model.savedAddressesView.loadResult
|
||||
let result = JSON.parse(resultRaw)
|
||||
if (result.o) {
|
||||
root.error = SavedAddresses.Error.None
|
||||
} else {
|
||||
root.error = parseInt(result.e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SavedAddressesError {
|
||||
id: errorMessage
|
||||
anchors.top: header.bottom
|
||||
anchors.topMargin: Style.current.padding
|
||||
visible: root.error !== SavedAddresses.Error.None
|
||||
text: getErrorText(root.error)
|
||||
height: visible ? 36 : 0
|
||||
}
|
||||
|
||||
StatusBaseText {
|
||||
anchors.top: errorMessage.bottom
|
||||
anchors.topMargin: Style.current.padding
|
||||
anchors.centerIn: parent
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
visible: listView.count === 0
|
||||
color: Theme.palette.baseColor1
|
||||
text: qsTr("No saved addresses")
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
anchors.top: errorMessage.bottom
|
||||
anchors.topMargin: Style.current.padding
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: Style.current.halfPadding
|
||||
anchors.right: parent.right
|
||||
anchors.left: parent.left
|
||||
visible: listView.count > 0
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||
|
||||
ListView {
|
||||
id: listView
|
||||
|
||||
clip: true
|
||||
spacing: 5
|
||||
anchors.fill: parent
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
|
||||
delegate: delegateSavedAddress
|
||||
|
||||
ListModel {
|
||||
id: exampleWalletModel
|
||||
ListElement {
|
||||
name: "Status account"
|
||||
address: "0xcfc9f08bbcbcb80760e8cb9a3c1232d19662fc6f"
|
||||
isFavorite: false
|
||||
}
|
||||
ListElement {
|
||||
name: "Test account 1"
|
||||
address: "0x2Ef1...E0Ba"
|
||||
isFavorite: false
|
||||
}
|
||||
ListElement {
|
||||
name: "Status account 2"
|
||||
address: "0x2Ef1...E0Ba"
|
||||
isFavorite: true
|
||||
}
|
||||
ListElement {
|
||||
name: "Status account"
|
||||
address: "0xcfc9f08bbcbcb80760e8cb9a3c1232d19662fc6f"
|
||||
isFavorite: false
|
||||
}
|
||||
ListElement {
|
||||
name: "Test account 1"
|
||||
address: "0x2Ef1...E0Ba"
|
||||
isFavorite: false
|
||||
}
|
||||
ListElement {
|
||||
name: "Status account 2"
|
||||
address: "0x2Ef1...E0Ba"
|
||||
isFavorite: true
|
||||
}
|
||||
ListElement {
|
||||
name: "Status account"
|
||||
address: "0xcfc9f08bbcbcb80760e8cb9a3c1232d19662fc6f"
|
||||
isFavorite: false
|
||||
}
|
||||
ListElement {
|
||||
name: "Test account 1"
|
||||
address: "0x2Ef1...E0Ba"
|
||||
isFavorite: false
|
||||
}
|
||||
ListElement {
|
||||
name: "Status account 2"
|
||||
address: "0x2Ef1...E0Ba"
|
||||
isFavorite: true
|
||||
}
|
||||
ListElement {
|
||||
name: "Status account"
|
||||
address: "0xcfc9f08bbcbcb80760e8cb9a3c1232d19662fc6f"
|
||||
isFavorite: false
|
||||
}
|
||||
ListElement {
|
||||
name: "Test account 1"
|
||||
address: "0x2Ef1...E0Ba"
|
||||
isFavorite: false
|
||||
}
|
||||
ListElement {
|
||||
name: "Status account 2"
|
||||
address: "0x2Ef1...E0Ba"
|
||||
isFavorite: true
|
||||
}
|
||||
}
|
||||
|
||||
model: walletV2Model.savedAddressesView.savedAddresses //exampleWalletModel
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -8,13 +8,23 @@ import "views/assets"
|
||||
import "."
|
||||
import "./components"
|
||||
|
||||
import StatusQ.Controls 0.1
|
||||
import StatusQ.Layout 0.1
|
||||
import StatusQ.Popups 0.1
|
||||
|
||||
Item {
|
||||
id: walletView
|
||||
|
||||
property bool hideSignPhraseModal: false
|
||||
|
||||
function showSavedAddressesView() {
|
||||
layoutWalletTwoPanel.rightPanel.view.replace(cmpSavedAddresses);
|
||||
}
|
||||
|
||||
function hideSavedAddressesView() {
|
||||
layoutWalletTwoPanel.rightPanel.view.replace(walletInfoContent);
|
||||
}
|
||||
|
||||
function openCollectibleDetailView(options) {
|
||||
collectiblesDetailPage.active = true
|
||||
collectiblesDetailPage.item.show(options)
|
||||
@ -37,6 +47,7 @@ Item {
|
||||
}
|
||||
|
||||
StatusAppTwoPanelLayout {
|
||||
id: layoutWalletTwoPanel
|
||||
anchors.top: seedPhraseWarning.bottom
|
||||
height: walletView.height - seedPhraseWarning.height
|
||||
width: walletView.width
|
||||
@ -54,6 +65,7 @@ Item {
|
||||
}
|
||||
|
||||
rightPanel: Item {
|
||||
property alias view: stackView
|
||||
anchors.fill: parent
|
||||
RowLayout {
|
||||
id: walletInfoContainer
|
||||
@ -149,6 +161,10 @@ Item {
|
||||
}
|
||||
}
|
||||
}
|
||||
Component {
|
||||
id: cmpSavedAddresses
|
||||
SavedAddresses {}
|
||||
}
|
||||
}
|
||||
|
||||
WalletFooter {
|
||||
|
137
ui/app/AppLayouts/WalletV2/components/AddEditSavedAddress.qml
Normal file
137
ui/app/AppLayouts/WalletV2/components/AddEditSavedAddress.qml
Normal file
@ -0,0 +1,137 @@
|
||||
import QtQuick 2.13
|
||||
import QtQuick.Controls 2.13
|
||||
import QtQuick.Dialogs 1.3
|
||||
|
||||
import "../../../../imports"
|
||||
import "../../../../shared"
|
||||
import "../../../../shared/status"
|
||||
|
||||
import StatusQ.Core 0.1
|
||||
import StatusQ.Core.Theme 0.1
|
||||
import StatusQ.Controls 0.1
|
||||
import StatusQ.Controls.Validators 0.1
|
||||
import StatusQ.Popups 0.1
|
||||
|
||||
StatusModal {
|
||||
id: root
|
||||
width: 574
|
||||
height: 490
|
||||
header.title: edit ? qsTr("Edit saved address") : qsTr("Add saved address")
|
||||
header.subTitle: edit ? name : qsTr("Unnamed")
|
||||
onOpened: {
|
||||
edit ?
|
||||
nameInput.input.edit.forceActiveFocus(Qt.MouseFocusReason) :
|
||||
addressInput.input.edit.forceActiveFocus(Qt.MouseFocusReason);
|
||||
}
|
||||
|
||||
property bool loading: false
|
||||
property var onBeforeSave: function() {}
|
||||
property bool edit: false
|
||||
property bool valid: addressInput.valid && nameInput.valid // TODO: Add network preference and emoji
|
||||
property bool dirty: addressInput.input.dirty && nameInput.input.dirty
|
||||
property alias address: addressInput.text
|
||||
property alias name: nameInput.text
|
||||
property int validationMode: edit ?
|
||||
StatusInput.ValidationMode.Always :
|
||||
StatusInput.ValidationMode.OnlyWhenDirty
|
||||
|
||||
contentItem: Column {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 8
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 10
|
||||
height: childrenRect.height
|
||||
|
||||
StatusInput {
|
||||
id: addressInput
|
||||
input.leftIcon: false
|
||||
input.implicitHeight: 56
|
||||
input.placeholderText: qsTr("Enter a valid address or ENS name")
|
||||
label: qsTr("Address")
|
||||
validators: [
|
||||
StatusAddressOrEnsValidator {
|
||||
errorMessage: qsTr("Invalid address or ENS name")
|
||||
},
|
||||
StatusMinLengthValidator {
|
||||
errorMessage: qsTr("Please provide an address or ENS name")
|
||||
}
|
||||
]
|
||||
validationMode: root.validationMode
|
||||
input.enabled: !root.edit
|
||||
}
|
||||
|
||||
Row {
|
||||
id: accountNameInputRow
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: 82
|
||||
spacing: 10
|
||||
Item {
|
||||
implicitWidth: 434
|
||||
height: parent.height
|
||||
StatusInput {
|
||||
id: nameInput
|
||||
anchors.fill: parent
|
||||
input.implicitHeight: 56
|
||||
input.placeholderText: qsTr("Enter a name")
|
||||
label: qsTr("Name")
|
||||
validators: [
|
||||
StatusMinLengthValidator {
|
||||
minLength: 1
|
||||
errorMessage: qsTr("Name must not be blank")
|
||||
}
|
||||
]
|
||||
validationMode: root.validationMode
|
||||
}
|
||||
}
|
||||
Item {
|
||||
//emoji placeholder
|
||||
width: 80
|
||||
height: parent.height
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 11
|
||||
StyledText {
|
||||
id: inputLabel
|
||||
text: "Emoji"
|
||||
font.weight: Font.Medium
|
||||
font.pixelSize: 13
|
||||
color: Style.current.textColor
|
||||
}
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 56
|
||||
anchors.top: inputLabel.bottom
|
||||
anchors.topMargin: 7
|
||||
radius: 10
|
||||
color: "pink"
|
||||
opacity: 0.6
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rightButtons: [
|
||||
StatusButton {
|
||||
text: root.edit ? qsTr("Save") : qsTr("Add address")
|
||||
enabled: !root.loading && root.valid && root.dirty
|
||||
loading: root.loading
|
||||
|
||||
MessageDialog {
|
||||
id: accountError
|
||||
title: qsTr("Adding the account failed")
|
||||
icon: StandardIcon.Critical
|
||||
standardButtons: StandardButton.Ok
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
root.loading = true;
|
||||
root.onBeforeSave()
|
||||
edit ?
|
||||
walletV2Model.savedAddressesView.editSavedAddress(name, address) :
|
||||
walletV2Model.savedAddressesView.addSavedAddress(name, address);
|
||||
root.close()
|
||||
root.loading = false;
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
import QtQuick 2.13
|
||||
import QtQuick.Controls 2.13
|
||||
|
||||
import "../../../../imports"
|
||||
|
||||
import StatusQ.Core 0.1
|
||||
import StatusQ.Core.Theme 0.1
|
||||
import StatusQ.Controls 0.1
|
||||
|
||||
Item {
|
||||
id: addEditError
|
||||
|
||||
property alias text: label.text
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
StatusIcon {
|
||||
id: errorIcon
|
||||
icon: "warning"
|
||||
color: Theme.palette.dangerColor1
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
}
|
||||
StatusBaseText {
|
||||
id: label
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: errorIcon.right
|
||||
anchors.leftMargin: Style.current.halfPadding
|
||||
font.pixelSize: 13
|
||||
color: Theme.palette.dangerColor1
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user