feat(@desktop/wallet): Wallet accounts - Account List

fixes #11189
This commit is contained in:
Khushboo Mehta 2023-06-22 12:32:43 +02:00 committed by Khushboo-dev-cpp
parent 620c7a746d
commit c47140a406
21 changed files with 365 additions and 127 deletions

View File

@ -39,3 +39,9 @@ proc isKeycardAccount*(self: Controller, account: WalletAccountDto): bool =
proc getWalletAccount*(self: Controller, address: string): WalletAccountDto =
return self.walletAccountService.getAccountByAddress(address)
proc getKeypairs*(self: Controller): seq[KeypairDto] =
return self.walletAccountService.getKeypairs()
proc getAllKnownKeycardsGroupedByKeyUid*(self: Controller): seq[KeycardDto] =
return self.walletAccountService.getAllKnownKeycardsGroupedByKeyUid()

View File

@ -3,6 +3,8 @@ import NimQml, sequtils, sugar, chronicles
import ./io_interface, ./view, ./item, ./controller
import ../io_interface as delegate_interface
import ../../../../shared/wallet_utils
import ../../../../shared/keypairs
import ../../../../shared_models/keypair_item
import ../../../../../global/global_singleton
import ../../../../../core/eventemitter
import ../../../../../../app_service/service/keycard/service as keycard_service
@ -44,6 +46,29 @@ method delete*(self: Module) =
method getModuleAsVariant*(self: Module): QVariant =
return self.viewVariant
method convertWalletAccountDtoToKeyPairAccountItem(self: Module, account: WalletAccountDto): KeyPairAccountItem =
result = newKeyPairAccountItem(
name = account.name,
path = account.path,
address = account.address,
pubKey = account.walletType,
emoji = account.emoji,
colorId = account.colorId,
icon = "",
balance = 0,
balanceFetched = false)
method createKeypairItems*(self: Module, walletAccounts: seq[WalletAccountDto]): seq[KeyPairItem] =
var keyPairItems = keypairs.buildKeyPairsList(self.controller.getKeypairs(), self.controller.getAllKnownKeycardsGroupedByKeyUid(),
excludeAlreadyMigratedPairs = false, excludePrivateKeyKeypairs = false)
var item = newKeyPairItem()
item.setIcon("show")
item.setPairType(KeyPairType.WatchOnly.int)
item.setAccounts(walletAccounts.filter(a => a.walletType == WalletTypeWatch).map(x => self.convertWalletAccountDtoToKeyPairAccountItem(x)))
keyPairItems.add(item)
return keyPairItems
method refreshWalletAccounts*(self: Module) =
let walletAccounts = self.controller.getWalletAccounts()
@ -52,6 +77,7 @@ method refreshWalletAccounts*(self: Module) =
walletAccountToWalletSettingsAccountsItem(w, keycardAccount)
))
self.view.setKeyPairModelItems(self.createKeypairItems(walletAccounts))
self.view.setItems(items)
method load*(self: Module) =

View File

@ -3,6 +3,7 @@ import NimQml, sequtils, strutils, sugar
import ./model
import ./item
import ./io_interface
import ../../../../shared_models/[keypair_model, keypair_item]
QtObject:
type
@ -10,10 +11,12 @@ QtObject:
delegate: io_interface.AccessInterface
accounts: Model
accountsVariant: QVariant
keyPairModel: KeyPairModel
proc delete*(self: View) =
self.accounts.delete
self.accountsVariant.delete
self.keyPairModel.delete
self.QObject.delete
proc newView*(delegate: io_interface.AccessInterface): View =
@ -22,6 +25,7 @@ QtObject:
result.delegate = delegate
result.accounts = newModel()
result.accountsVariant = newQVariant(result.accounts)
result.keyPairModel = newKeyPairModel()
proc load*(self: View) =
self.delegate.viewDidLoad()
@ -43,6 +47,18 @@ QtObject:
proc onUpdatedAccount*(self: View, account: Item) =
self.accounts.onUpdatedAccount(account)
self.keyPairModel.onUpdatedAccount(account.keyUid, account.address, account.name, account.colorId, account.emoji)
proc deleteAccount*(self: View, address: string) {.slot.} =
self.delegate.deleteAccount(address)
proc keyPairModelChanged*(self: View) {.signal.}
proc getKeyPairModel(self: View): QVariant {.slot.} =
return newQVariant(self.keyPairModel)
QtProperty[QVariant] keyPairModel:
read = getKeyPairModel
notify = keyPairModelChanged
proc setKeyPairModelItems*(self: View, items: seq[KeyPairItem]) =
self.keyPairModel.setItems(items)
self.keyPairModelChanged()

View File

@ -6,17 +6,23 @@ type
layer: int
chainName: string
iconUrl: string
shortName: string
chainColor: string
proc initItem*(
chainId: int,
layer: int,
chainName: string,
iconUrl: string,
shortName: string,
chainColor: string,
): Item =
result.chainId = chainId
result.layer = layer
result.chainName = chainName
result.iconUrl = iconUrl
result.shortName = shortName
result.chainColor = chainColor
proc `$`*(self: Item): string =
result = fmt"""NetworkItem(
@ -24,6 +30,8 @@ proc `$`*(self: Item): string =
chainName: {self.chainName},
layer: {self.layer},
iconUrl:{self.iconUrl},
shortName: {self.shortName},
chainColor: {self.chainColor},
]"""
proc getChainId*(self: Item): int =
@ -36,4 +44,10 @@ proc getChainName*(self: Item): string =
return self.chainName
proc getIconURL*(self: Item): string =
return self.iconUrl
return self.iconUrl
proc getShortName*(self: Item): string =
return self.shortName
proc getChainColor*(self: Item): string =
return self.chainColor

View File

@ -10,6 +10,8 @@ type
Layer
ChainName
IconUrl
ShortName
ChainColor
QtObject:
type
@ -49,6 +51,8 @@ QtObject:
ModelRole.Layer.int:"layer",
ModelRole.ChainName.int:"chainName",
ModelRole.IconUrl.int:"iconUrl",
ModelRole.ShortName.int:"shortName",
ModelRole.ChainColor.int:"chainColor",
}.toTable
method data(self: Model, index: QModelIndex, role: int): QVariant =
@ -70,6 +74,10 @@ QtObject:
result = newQVariant(item.getChainName())
of ModelRole.IconUrl:
result = newQVariant(item.getIconURL())
of ModelRole.ShortName:
result = newQVariant(item.getShortName())
of ModelRole.ChainColor:
result = newQVariant(item.getChainColor())
proc rowData*(self: Model, index: int, column: string): string {.slot.} =
if (index >= self.items.len):
@ -80,9 +88,17 @@ QtObject:
of "layer": result = $item.getLayer()
of "chainName": result = $item.getChainName()
of "iconUrl": result = $item.getIconURL()
of "shortName": result = $item.getShortName()
of "chainColor": result = $item.getChainColor()
proc setItems*(self: Model, items: seq[Item]) =
self.beginResetModel()
self.items = items
self.endResetModel()
self.countChanged()
self.countChanged()
proc getAllNetworksSupportedPrefix*(self: Model): string =
var networkString = ""
for item in self.items:
networkString = networkString & item.getShortName() & ':'
return networkString

View File

@ -59,7 +59,12 @@ QtObject:
n.layer,
n.chainName,
n.iconUrl,
n.shortName,
n.chainColor
))
self.networks.setItems(items)
self.delegate.viewDidLoad()
self.delegate.viewDidLoad()
proc getAllNetworksSupportedPrefix*(self: View): string {.slot.} =
return self.networks.getAllNetworksSupportedPrefix()

View File

@ -74,7 +74,7 @@ proc buildKeyPairsList*(keypairs: seq[KeypairDto], allMigratedKeypairs: seq[Keyc
locked = false,
name = kp.name,
image = "",
icon = if keyPairMigrated(kp.keyUid): "keycard" else: "key_pair_private_key",
icon = if keyPairMigrated(kp.keyUid): "keycard" else: "objects",
pairType = KeyPairType.PrivateKeyImport,
derivedFrom = kp.derivedFrom,
lastUsedDerivationIndex = kp.lastUsedDerivationIndex,

View File

@ -9,6 +9,7 @@ type
Profile
SeedImport
PrivateKeyImport
WatchOnly
QtObject:
type KeyPairItem* = ref object of QObject

View File

@ -73,4 +73,10 @@ QtObject:
for i in 0 ..< self.items.len:
if(self.items[i].getKeyUid() == keyUid):
return self.items[i]
return nil
return nil
proc onUpdatedAccount*(self: KeyPairModel, keyUid, address, name, colorId, emoji: string) =
for item in self.items:
if keyUid == item.getKeyUid():
item.getAccountsModel().updateDetailsForAddressIfTheyAreSet(address, name, colorId, emoji)
break

View File

@ -389,4 +389,8 @@ ListModel {
title: "ActivityFilterMenu"
section: "Wallet"
}
ListElement {
title: "ProfileAccounts"
section: "Wallet"
}
}

View File

@ -0,0 +1,54 @@
import QtQuick 2.14
import QtQuick.Controls 2.14
import AppLayouts.Profile.controls 1.0
import StatusQ.Core 0.1
import utils 1.0
import Storybook 1.0
import Models 1.0
SplitView {
id: root
Logs { id: logs }
orientation: Qt.Vertical
QtObject {
id: d
readonly property QtObject walletStore: QtObject {
property string userProfilePublicKey: "zq3shfrgk6swgrrnc7wmwun1gvgact9iaevv9xwirumimhbyf"
function getAllNetworksSupportedString(hovered) {
return hovered ? "<font color=\"" + "#627EEA" + "\">" + "eth:" + "</font>" +
"<font color=\"" + "#E90101" + "\">" + "opt:" + "</font>" +
"<font color=\"" + "#27A0EF" + "\">" + "arb:" + "</font>" : "eth:opt:arb:"
}
}
}
Item {
SplitView.fillWidth: true
SplitView.fillHeight: true
StatusListView {
width: 500
height: parent.height
anchors.verticalCenterOffset: 20
anchors.centerIn: parent
spacing: 24
model: WalletKeyPairModel {}
delegate: WalletKeyPairDelegate {
width: parent.width
chainShortNames: d.walletStore.getAllNetworksSupportedString()
userProfilePublicKey: d.walletStore.userProfilePublicKey
onGoToAccountView: console.warn("onGoToAccountView ::")
}
}
}
}

View File

@ -0,0 +1,52 @@
import QtQuick 2.15
ListModel {
readonly property var data: [
{
keyPair: {
keyUid: "",
pubKey: "zq3shfrgk6swgrrnc7wmwun1gvgact9iaevv9xwirumimhbyf",
name: "Mike",
image: "",
icon: "",
pairType: 0,
migratedToKeycard: false,
accounts: accountsList
}
},
{
keyPair: {
keyUid: "",
pubKey: "",
name: "Seed Phrase",
image: "",
icon: "key_pair_private_key",
pairType: 1,
migratedToKeycard: true,
accounts: accountsList
}
},
{
keyPair: {
keyUid: "",
pubKey: "",
name: "",
image: "",
icon: "show",
pairType: 3,
migratedToKeycard: false,
accounts: accountsList
}
}
]
property var accountsList: ListModel {
readonly property var data1: [
{account: { name: "Test account", emoji: "😋", colorId: "primary", address: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240" }},
{account: { name: "Another account", emoji: "🚗", colorId: "army", address: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8888"}}
]
Component.onCompleted: append(data1)
}
Component.onCompleted: append(data)
}

View File

@ -14,3 +14,4 @@ singleton ModelsData 1.0 ModelsData.qml
singleton NetworksModel 1.0 NetworksModel.qml
singleton PermissionsModel 1.0 PermissionsModel.qml
RecipientModel 1.0 RecipientModel.qml
WalletKeyPairModel 1.0 WalletKeyPairModel.qml

View File

@ -168,7 +168,7 @@ class SettingsScreen:
raise Exception("Account not found")
accounts = get_obj(WalletSettingsScreen.GENERATED_ACCOUNTS.value)
click_obj(accounts.itemAtIndex(index))
click_obj(accounts.itemAt(index))
click_obj_by_name(WalletSettingsScreen.DELETE_ACCOUNT.value)
click_obj_by_name(WalletSettingsScreen.DELETE_ACCOUNT_CONFIRM.value)
@ -180,7 +180,7 @@ class SettingsScreen:
def verify_address(self, address: str):
accounts = get_obj(WalletSettingsScreen.GENERATED_ACCOUNTS.value)
verify_text_matching_insensitive(accounts.itemAtIndex(0).statusListItemSubTitle, address)
verify_text_matching_insensitive(accounts.itemAt(0).statusListItemSubTitle, address)
# Post condition: Messaging Settings is visible (@see StatusMainScreen.open_settings)
def open_messaging_settings(self):
@ -245,7 +245,7 @@ class SettingsScreen:
def _find_account_index(self, account_name: str) -> int:
accounts = get_obj(WalletSettingsScreen.GENERATED_ACCOUNTS.value)
for index in range(accounts.count):
if (accounts.itemAtIndex(index).objectName == account_name):
if (accounts.itemAt(index).objectName == account_name):
return index
return -1
@ -254,7 +254,7 @@ class SettingsScreen:
def select_default_account(self):
accounts = get_obj(WalletSettingsScreen.GENERATED_ACCOUNTS.value)
click_obj(accounts.itemAtIndex(0))
click_obj(accounts.itemAt(0))
click_obj_by_name(WalletSettingsScreen.EDIT_ACCOUNT_BUTTON.value)
def edit_account(self, account_name: str, account_color: str):

View File

@ -91,7 +91,7 @@ edit_TextEdit = {"container": statusDesktop_mainWindow_overlay, "type": "TextEdi
mainWallet_Saved_Addreses_More_Edit = {"container": statusDesktop_mainWindow, "objectName": "editroot", "type": "StatusMenuItem"}
mainWallet_Saved_Addreses_More_Delete = {"container": statusDesktop_mainWindow, "objectName": "deleteSavedAddress", "type": "StatusMenuItem"}
mainWallet_Saved_Addreses_More_Confirm_Delete = {"container": statusDesktop_mainWindow, "objectName": "confirmDeleteSavedAddress", "type": "StatusButton"}
settings_Wallet_MainView_GeneratedAccounts = {"container": statusDesktop_mainWindow, "objectName": 'generatedAccounts', "type": 'ListView'}
settings_Wallet_MainView_GeneratedAccounts = {"container": statusDesktop_mainWindow, "objectName": 'generatedAccounts', "type": 'Repeater'}
settings_Wallet_AccountView_DeleteAccount = {"container": statusDesktop_mainWindow, "type": "StatusButton", "objectName": "deleteAccountButton"}
settings_Wallet_AccountView_DeleteAccount_Confirm = {"container": statusDesktop_mainWindow, "type": "StatusButton", "objectName": "confirmDeleteAccountButton"}
mainWindow_ScrollView_2 = {"container": statusDesktop_mainWindow, "occurrence": 2, "type": "StatusScrollView", "unnamed": 1, "visible": True}

View File

@ -1,6 +1,11 @@
import QtQuick 2.14
import StatusQ.Components 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Core 0.1
import StatusQ.Core.Utils 0.1 as StatusQUtils
import AppLayouts.Wallet 1.0
import utils 1.0
@ -8,14 +13,18 @@ StatusListItem {
id: root
property var account
property bool showShevronIcon: true
property string chainShortNames
property int totalCount: 0
signal goToAccountView()
title: account.name
subTitle: account.address
objectName: account.name
asset.color: Utils.getColorForId(account.colorId)
title: account.name
subTitle: {
const elidedAddress = StatusQUtils.Utils.elideText(account.address,6,4)
return sensor.containsMouse ? WalletUtils.colorizedChainPrefix(chainShortNames) + Utils.richColorText(elidedAddress, Theme.palette.directColor1) : elidedAddress
}
asset.color: !!account.colorId ? Utils.getColorForId(account.colorId): ""
asset.emoji: account.emoji
asset.name: !account.emoji ? "filled-account": ""
asset.letterSize: 14
@ -24,16 +33,22 @@ StatusListItem {
asset.width: 40
asset.height: 40
components: !showShevronIcon ? [] : [ shevronIcon ]
onClicked: {
goToAccountView()
}
StatusIcon {
id: shevronIcon
visible: root.showShevronIcon
components: StatusIcon {
icon: "next"
color: Theme.palette.baseColor1
}
onClicked: goToAccountView()
// This is used to give the first and last delgate rounded corners
Rectangle {
visible: totalCount > 1
readonly property bool isLastOrFirstItem: index === 0 || index === (totalCount-1)
width: parent.width
height: isLastOrFirstItem? parent.height/2 : parent.height
anchors.top: !isLastOrFirstItem || index === (totalCount-1) ? parent.top: undefined
anchors.bottom: index === 0 ? parent.bottom: undefined
color: parent.color
z: parent.z - 10
}
}

View File

@ -0,0 +1,93 @@
import QtQuick 2.14
import QtQuick.Layouts 1.14
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import utils 1.0
Rectangle {
id: root
property string chainShortNames
property string userProfilePublicKey
signal goToAccountView(var account)
QtObject {
id: d
readonly property var relatedAccounts: model.keyPair.accounts
readonly property bool isWatchOnly: model.keyPair.pairType === Constants.keycard.keyPairType.watchOnly
readonly property bool isPrivateKeyImport: model.keyPair.pairType === Constants.keycard.keyPairType.privateKeyImport
readonly property bool isProfileKeypair: model.keyPair.pairType === Constants.keycard.keyPairType.profile
readonly property string locationInfo: model.keyPair.migratedToKeycard ? qsTr("On Keycard"): qsTr("On device")
}
implicitHeight: layout.height
color: Theme.palette.baseColor4
radius: 8
ColumnLayout {
id: layout
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width
StatusListItem {
Layout.fillWidth: true
title: d.isWatchOnly ? qsTr("Watch only") : model.keyPair.name
statusListItemSubTitle.textFormat: Qt.RichText
titleTextIcon: model.keyPair.migratedToKeycard ? "keycard": ""
subTitle: d.isWatchOnly ? "" : d.isProfileKeypair ?
Utils.getElidedCompressedPk(model.keyPair.pubKey) + Constants.settingsSection.dotSepString + d.locationInfo : d.locationInfo
color: Theme.palette.transparent
ringSettings {
ringSpecModel: d.isProfileKeypair ? Utils.getColorHashAsJson(root.userProfilePublicKey) : []
ringPxSize: Math.max(asset.width / 24.0)
}
asset {
width: model.keyPair.icon ? 24 : 40
height: model.keyPair.icon ? 24 : 40
name: model.keyPair.image ? model.keyPair.image : model.keyPair.icon
isImage: !!model.keyPair.image
color: d.isProfileKeypair ? Utils.colorForPubkey(root.userProfilePublicKey) : Theme.palette.primaryColor1
letterSize: Math.max(4, asset.width / 2.4)
charactersLen: 2
isLetterIdenticon: !model.keyPair.icon && !asset.name.toString()
}
components: [
StatusFlatRoundButton {
icon.name: "more"
icon.color: Theme.palette.directColor1
visible: !d.isWatchOnly
},
StatusBaseText {
anchors.verticalCenter: parent.verticalCenter
text: qsTr("Include in total balance")
visible: d.isWatchOnly
},
StatusSwitch {
visible: d.isWatchOnly
// To-do connect in different task
// checked: false
// onCheckedChanged: {}
}
]
}
StatusListView {
Layout.fillWidth: true
Layout.preferredHeight: childrenRect.height
Layout.leftMargin: 16
Layout.rightMargin: 16
spacing: 1
model: d.relatedAccounts
delegate: WalletAccountDelegate {
width: ListView.view.width
account: model.account
totalCount: ListView.view.count
chainShortNames: root.chainShortNames
onGoToAccountView: root.goToAccountView(model.account)
}
}
}
}

View File

@ -4,3 +4,4 @@ AccountShowcaseDelegate 1.0 AccountShowcaseDelegate.qml
AssetShowcaseDelegate 1.0 AssetShowcaseDelegate.qml
WalletAccountDelegate 1.0 WalletAccountDelegate.qml
StaticSocialLinkInput 1.0 StaticSocialLinkInput.qml
WalletKeyPairDelegate 1.0 WalletKeyPairDelegate.qml

View File

@ -22,6 +22,9 @@ QtObject {
property var flatCollectibles: Global.appIsReady ? walletSectionCollectibles.model : null
property var assets: walletSectionAssets.assets
property var accounts: Global.appIsReady? accountsModule.accounts : null
property var originModel: accountsModule.keyPairModel
property string userProfilePublicKey: userProfile.pubKey
function deleteAccount(address) {
return accountsModule.deleteAccount(address)
@ -48,4 +51,8 @@ QtObject {
function loadDapps() {
dappPermissionsModule.loadDapps()
}
function getAllNetworksSupportedPrefix() {
return networksModule.getAllNetworksSupportedPrefix()
}
}

View File

@ -27,9 +27,9 @@ Column {
root.walletStore.loadDapps()
}
Separator {
height: 17
}
spacing: 8
Separator {}
StatusListItem {
title: qsTr("DApp Permissions")
@ -45,9 +45,7 @@ Column {
]
}
Separator {
height: 17
}
Separator {}
StatusListItem {
objectName: "networksItem"
@ -63,105 +61,25 @@ Column {
]
}
Separator {
height: 17
}
Separator {}
StatusDescriptionListItem {
height: 64
subTitle: qsTr("Accounts")
}
StatusSectionHeadline {
text: qsTr("Generated from Your Seed Phrase")
leftPadding: Style.current.padding
topPadding: Style.current.halfPadding
bottomPadding: Style.current.halfPadding/2
}
ListView {
width: parent.width
height: childrenRect.height
objectName: "generatedAccounts"
model: SortFilterProxyModel {
sourceModel: walletStore.accounts
filters: ExpressionFilter {
expression: {
return model.walletType === "generated" || model.walletType === ""
}
}
}
delegate: WalletAccountDelegate {
width: ListView.view.width
account: model
onGoToAccountView: {
root.goToAccountView(model)
}
}
}
SortFilterProxyModel {
id: importedAccounts
sourceModel: walletStore.accounts
filters: ExpressionFilter {
expression: {
return model.walletType !== "generated" && model.walletType !== "watch" && model.walletType !== ""
}
}
}
StatusSectionHeadline {
text: qsTr("Imported")
leftPadding: Style.current.padding
topPadding: Style.current.halfPadding
bottomPadding: Style.current.halfPadding/2
visible: importedAccounts.count > 0
}
Repeater {
width: parent.width
model: importedAccounts
delegate: WalletAccountDelegate {
width: parent.width
account: model
onGoToAccountView: {
root.goToAccountView(model)
}
}
}
SortFilterProxyModel {
id: watchOnlyAccounts
sourceModel: walletStore.accounts
filters: ValueFilter {
roleName: "walletType"
value: "watch"
}
}
StatusSectionHeadline {
text: qsTr("Watch-Only")
leftPadding: Style.current.padding
topPadding: Style.current.halfPadding
bottomPadding: Style.current.halfPadding/2
visible: watchOnlyAccounts.count > 0
}
Repeater {
width: parent.width
model: watchOnlyAccounts
delegate: WalletAccountDelegate {
width: parent.width
account: model
onGoToAccountView: {
root.goToAccountView(model)
}
}
}
// Adding padding to the end so that when the view is scrolled to the end there is some gap left
Item {
height: Style.current.bigPadding
width: parent.width
height: 8
}
Column {
width: parent.width
spacing: 24
Repeater {
objectName: "generatedAccounts"
model: walletStore.originModel
delegate: WalletKeyPairDelegate {
width: parent.width
chainShortNames: walletStore.getAllNetworksSupportedPrefix()
userProfilePublicKey: walletStore.userProfilePublicKey
onGoToAccountView: root.goToAccountView(account)
}
}
}
}

View File

@ -545,6 +545,8 @@ QtObject {
readonly property int oneToOneChat: 1
readonly property int groupChat: 2
}
property string dotSepString: '<font size="3"> &#x2022; </font>'
}
readonly property QtObject ephemeralNotificationType: QtObject {
@ -601,6 +603,7 @@ QtObject {
readonly property int profile: 0
readonly property int seedImport: 1
readonly property int privateKeyImport: 2
readonly property int watchOnly: 3
}
readonly property QtObject shared: QtObject {