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:
Eric Mastro 2021-09-20 13:00:50 +10:00 committed by Iuri Matias
parent e6f8a79f67
commit 9854a49a44
11 changed files with 920 additions and 35 deletions

View File

@ -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()

View 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

View 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()

View File

@ -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 {

View File

@ -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 */

View File

@ -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 {

View File

@ -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();
}
}
}

View 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
}
}
}

View File

@ -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 {

View 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;
}
}
]
}

View File

@ -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
}
}