mirror of
https://github.com/status-im/status-app.git
synced 2026-08-27 15:11:20 +00:00
refactor(wallet): AssetsView on the terminal model — retire proxy chain
This commit is contained in:
@@ -1,248 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import StatusQ.Models
|
||||
|
||||
import Storybook
|
||||
|
||||
import utils
|
||||
import shared.views
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
ListModel {
|
||||
id: listModel
|
||||
|
||||
readonly property var data: [
|
||||
{
|
||||
key: Constants.ethGroupKey,
|
||||
name: "Ether",
|
||||
symbol: "ETH",
|
||||
balances: [
|
||||
{
|
||||
chainId: "chain_id_1",
|
||||
balance: "186316672770338050",
|
||||
account: "account_1",
|
||||
},
|
||||
{
|
||||
chainId: "chain_id_1",
|
||||
balance: "386318672772348050",
|
||||
account: "account_2",
|
||||
},
|
||||
{
|
||||
chainId: "chain_id_2",
|
||||
balance: "186311232772348990",
|
||||
account: "account_1",
|
||||
},
|
||||
{
|
||||
chainId: "chain_id_2",
|
||||
balance: "986317232772348990",
|
||||
account: "account_1",
|
||||
}
|
||||
],
|
||||
decimals: 18,
|
||||
communityId: "",
|
||||
communityName: "",
|
||||
communityImage: "",
|
||||
marketDetails: {
|
||||
changePct24hour: -2.1232,
|
||||
currencyPrice: {
|
||||
amount: 3423.23898
|
||||
}
|
||||
},
|
||||
detailsLoading: false,
|
||||
logoUri: "",
|
||||
position: 1,
|
||||
visible: true
|
||||
},
|
||||
{
|
||||
key: Constants.sntGroupKey,
|
||||
name: "Status",
|
||||
symbol: "SNT",
|
||||
balances: [
|
||||
{
|
||||
chainId: "chain_id_1",
|
||||
balance: "386316672770338850",
|
||||
account: "account_1",
|
||||
},
|
||||
{
|
||||
chainId: "chain_id_1",
|
||||
balance: "377778672772348050",
|
||||
account: "account_2",
|
||||
},
|
||||
{
|
||||
chainId: "chain_id_2",
|
||||
balance: "146311232772348990",
|
||||
account: "account_1",
|
||||
},
|
||||
{
|
||||
chainId: "chain_id_3",
|
||||
balance: "86317232772348990",
|
||||
account: "account_1",
|
||||
}
|
||||
],
|
||||
decimals: 18,
|
||||
communityId: "",
|
||||
communityName: "",
|
||||
communityImage: "",
|
||||
marketDetails: {
|
||||
changePct24hour: 9.232,
|
||||
currencyPrice: {
|
||||
amount: 33.23898
|
||||
}
|
||||
},
|
||||
detailsLoading: false,
|
||||
logoUri: "",
|
||||
position: 2,
|
||||
visible: true
|
||||
},
|
||||
{
|
||||
key: "key_MYASST",
|
||||
name: "Community Asset",
|
||||
symbol: "MYASST",
|
||||
balances: [
|
||||
{
|
||||
chainId: "chain_id_1",
|
||||
balance: "23234",
|
||||
account: "account_1",
|
||||
},
|
||||
{
|
||||
chainId: "chain_id_1",
|
||||
balance: "63234",
|
||||
account: "account_2",
|
||||
}
|
||||
],
|
||||
decimals: 3,
|
||||
communityId: "0x033f36ccb",
|
||||
communityName: "My Community",
|
||||
communityImage: Constants.tokenIcon("DAI", false),
|
||||
marketDetails: {
|
||||
changePct24hour: 0,
|
||||
currencyPrice: {
|
||||
amount: 0
|
||||
}
|
||||
},
|
||||
detailsLoading: false,
|
||||
logoUri: Constants.tokenIcon("ZRX", false),
|
||||
position: 5,
|
||||
visible: true
|
||||
}
|
||||
]
|
||||
|
||||
Component.onCompleted: {
|
||||
append(data)
|
||||
|
||||
const chains = new Set()
|
||||
const accounts = new Set()
|
||||
|
||||
data.forEach(e => e.balances.forEach(
|
||||
e => { chains.add(e.chainId);
|
||||
accounts.add(e.account)}))
|
||||
|
||||
chainsSelector.model = [...chains.values()]
|
||||
chainsDownSelector.model = [...chains.values()]
|
||||
accountsSelector.model = [...accounts.values()]
|
||||
}
|
||||
}
|
||||
|
||||
AssetsViewAdaptor {
|
||||
id: adaptor
|
||||
|
||||
chains: chainsSelector.selection
|
||||
accounts: accountsSelector.selection
|
||||
|
||||
marketValueThreshold: minimumBalanceSlider.value
|
||||
|
||||
chainsError: chains => {
|
||||
const chainsDown = chainsDownSelector.selection
|
||||
const downForToken = chains.filter(value => chainsDown.includes(value))
|
||||
|
||||
if (downForToken.length)
|
||||
return "Chains down: " + JSON.stringify(downForToken)
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
tokensModel: listModel
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
|
||||
Label { text: "CHAINS:" }
|
||||
|
||||
CheckBoxFlowSelector {
|
||||
id: chainsSelector
|
||||
|
||||
Layout.fillWidth: true
|
||||
initialSelection: true
|
||||
}
|
||||
|
||||
Label { text: "CHAINS DOWN:" }
|
||||
|
||||
CheckBoxFlowSelector {
|
||||
id: chainsDownSelector
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Label { text: "ACCOUNTS:" }
|
||||
|
||||
CheckBoxFlowSelector {
|
||||
id: accountsSelector
|
||||
|
||||
Layout.fillWidth: true
|
||||
initialSelection: true
|
||||
}
|
||||
|
||||
Label { text: "MINIMUM BALANCE:" }
|
||||
|
||||
RowLayout {
|
||||
Slider {
|
||||
id: minimumBalanceSlider
|
||||
|
||||
from: 0.1
|
||||
to: 100
|
||||
|
||||
value: 10
|
||||
}
|
||||
Label {
|
||||
text: minimumBalanceSlider.value
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
GenericListView {
|
||||
label: "Input model"
|
||||
|
||||
model: listModel
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
skipEmptyRoles: true
|
||||
}
|
||||
|
||||
GenericListView {
|
||||
label: "Adapter's output model"
|
||||
|
||||
model: adaptor.model
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
roles:
|
||||
["key", "symbol", "name", "icon", "error", "balance", "balanceText",
|
||||
"marketDetailsAvailable", "marketDetailsLoading",
|
||||
"marketPrice", "marketChangePct24hour", "communityId",
|
||||
"communityName", "communityIcon", "position", "canBeHidden"]
|
||||
|
||||
skipEmptyRoles: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// category: Adaptors
|
||||
@@ -12,28 +12,72 @@ import Storybook
|
||||
import AppLayouts.Wallet.controls
|
||||
import AppLayouts.Wallet.panels
|
||||
|
||||
import StatusQ
|
||||
import StatusQ.Popups.Dialog
|
||||
|
||||
import SortFilterProxyModel
|
||||
|
||||
SplitView {
|
||||
id: root
|
||||
|
||||
ListModel {
|
||||
function format(amount, symbol) {
|
||||
return `${amount.toLocaleString(Qt.locale())} ${symbol}`
|
||||
}
|
||||
|
||||
// Role-compatible stub for the terminal AssetsAdaptorModel: derives
|
||||
// isCommunity/marketBalance/change1DayFiat, filters to visible rows and
|
||||
// re-sorts in place via sortBy(roleName, order) — mirrors the Nim model.
|
||||
SortFilterProxyModel {
|
||||
id: assetsModel
|
||||
|
||||
function format(amount, symbol) {
|
||||
return `${amount.toLocaleString(Qt.locale())} ${symbol}`
|
||||
property int sortRoleOrder: Qt.DescendingOrder
|
||||
property string sortRoleName: "name"
|
||||
|
||||
function sortBy(roleName, order) {
|
||||
assetsModel.sortRoleName = roleName
|
||||
assetsModel.sortRoleOrder = order
|
||||
}
|
||||
|
||||
sourceModel: baseAssetsModel
|
||||
proxyRoles: [
|
||||
FastExpressionRole {
|
||||
name: "isCommunity"
|
||||
expression: !!model.communityId ? "community" : ""
|
||||
expectedRoles: ["communityId"]
|
||||
},
|
||||
FastExpressionRole {
|
||||
name: "marketBalance"
|
||||
expression: model.balance * model.marketPrice
|
||||
expectedRoles: ["balance", "marketPrice"]
|
||||
},
|
||||
FastExpressionRole {
|
||||
name: "change1DayFiat"
|
||||
expression: model.marketBalance * (1 - (1 / (model.marketChangePct24hour / 100 + 1)))
|
||||
expectedRoles: ["marketBalance", "marketChangePct24hour"]
|
||||
}
|
||||
]
|
||||
filters: ValueFilter { roleName: "visible"; value: true }
|
||||
sorters: [
|
||||
RoleSorter { roleName: "isCommunity" },
|
||||
RoleSorter {
|
||||
roleName: assetsModel.sortRoleName
|
||||
sortOrder: assetsModel.sortRoleOrder
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
ListModel {
|
||||
id: baseAssetsModel
|
||||
|
||||
Component.onCompleted: {
|
||||
const data = [
|
||||
{
|
||||
key: "key_ETH",
|
||||
symbol: "ETH",
|
||||
name: "Ether",
|
||||
icon: Constants.tokenIcon("ETH", false),
|
||||
logoUri: Constants.tokenIcon("ETH", false),
|
||||
balance: 10.0,
|
||||
balanceText: format(10.0, "ETH"),
|
||||
error: "",
|
||||
balanceLoading: false,
|
||||
|
||||
marketDetailsAvailable: true,
|
||||
marketDetailsLoading: true,
|
||||
@@ -42,19 +86,20 @@ SplitView {
|
||||
|
||||
communityId: "",
|
||||
communityName: "",
|
||||
communityIcon: Qt.resolvedUrl(""),
|
||||
communityImage: Qt.resolvedUrl(""),
|
||||
|
||||
position: 2,
|
||||
canBeHidden: false
|
||||
canBeHidden: false,
|
||||
visible: true,
|
||||
chainIds: "1"
|
||||
},
|
||||
{
|
||||
key: "key_SNT",
|
||||
symbol: "SNT",
|
||||
name: "Status",
|
||||
icon: Constants.tokenIcon("SNT", false),
|
||||
logoUri: Constants.tokenIcon("SNT", false),
|
||||
balance: 20023.0,
|
||||
balanceText: format(20023.0, "SNT"),
|
||||
error: "",
|
||||
balanceLoading: false,
|
||||
|
||||
marketDetailsAvailable: true,
|
||||
marketDetailsLoading: false,
|
||||
@@ -63,19 +108,20 @@ SplitView {
|
||||
|
||||
communityId: "",
|
||||
communityName: "",
|
||||
communityIcon: Qt.resolvedUrl(""),
|
||||
communityImage: Qt.resolvedUrl(""),
|
||||
|
||||
position: 1,
|
||||
canBeHidden: true
|
||||
canBeHidden: true,
|
||||
visible: true,
|
||||
chainIds: "1,10"
|
||||
},
|
||||
{
|
||||
key: "key_MCT",
|
||||
symbol: "MCT",
|
||||
name: "My custom token",
|
||||
icon: Constants.tokenIcon("ZRX", false),
|
||||
logoUri: Constants.tokenIcon("ZRX", false),
|
||||
balance: 102.4,
|
||||
balanceText: format(102.4, "MCT"),
|
||||
error: "",
|
||||
balanceLoading: false,
|
||||
|
||||
marketDetailsAvailable: false,
|
||||
marketDetailsLoading: false,
|
||||
@@ -84,19 +130,20 @@ SplitView {
|
||||
|
||||
communityId: "34",
|
||||
communityName: "Crypto Kitties",
|
||||
communityIcon: Constants.tokenIcon("DAI", false),
|
||||
communityImage: Constants.tokenIcon("DAI", false),
|
||||
|
||||
position: 4,
|
||||
canBeHidden: true
|
||||
canBeHidden: true,
|
||||
visible: true,
|
||||
chainIds: "1"
|
||||
},
|
||||
{
|
||||
key: "key_DAI",
|
||||
symbol: "DAI",
|
||||
name: "Dai",
|
||||
icon: Constants.tokenIcon("DAI", false),
|
||||
logoUri: Constants.tokenIcon("DAI", false),
|
||||
balance: 123.24,
|
||||
balanceText: format(123.24, "DAI"),
|
||||
error: "",
|
||||
balanceLoading: false,
|
||||
|
||||
marketDetailsAvailable: true,
|
||||
marketDetailsLoading: false,
|
||||
@@ -105,19 +152,20 @@ SplitView {
|
||||
|
||||
communityId: "",
|
||||
communityName: "",
|
||||
communityIcon: Qt.resolvedUrl(""),
|
||||
communityImage: Qt.resolvedUrl(""),
|
||||
|
||||
position: 3,
|
||||
canBeHidden: true
|
||||
canBeHidden: true,
|
||||
visible: true,
|
||||
chainIds: "1"
|
||||
},
|
||||
{
|
||||
key: "key_USDT",
|
||||
symbol: "USDT",
|
||||
name: "USDT",
|
||||
icon: Constants.tokenIcon("USDT", false),
|
||||
logoUri: Constants.tokenIcon("USDT", false),
|
||||
balance: 15.24,
|
||||
balanceText: format(15.24, "USDT"),
|
||||
error: "",
|
||||
balanceLoading: false,
|
||||
|
||||
marketDetailsAvailable: true,
|
||||
marketDetailsLoading: false,
|
||||
@@ -126,21 +174,20 @@ SplitView {
|
||||
|
||||
communityId: "",
|
||||
communityName: "",
|
||||
communityIcon: Qt.resolvedUrl(""),
|
||||
communityImage: Qt.resolvedUrl(""),
|
||||
|
||||
position: 5,
|
||||
canBeHidden: true
|
||||
canBeHidden: true,
|
||||
visible: true,
|
||||
chainIds: "1"
|
||||
},
|
||||
{
|
||||
key: "key_TBT",
|
||||
symbol: "TBT",
|
||||
name: "The best token",
|
||||
icon: Constants.tokenIcon("UNI", false),
|
||||
logoUri: Constants.tokenIcon("UNI", false),
|
||||
balance: 102,
|
||||
balanceText: format(102, "TBT"),
|
||||
error: "Pocket Network (POKT) & Infura are currently both "
|
||||
+ "unavailable for %1. %1 balances are as of %2."
|
||||
.arg("TBT").arg("10/06/2024"),
|
||||
balanceLoading: false,
|
||||
|
||||
marketDetailsAvailable: false,
|
||||
marketDetailsLoading: false,
|
||||
@@ -149,10 +196,12 @@ SplitView {
|
||||
|
||||
communityId: "3423",
|
||||
communityName: "Best tokens",
|
||||
communityIcon: Constants.tokenIcon("UNI", false),
|
||||
communityImage: Constants.tokenIcon("UNI", false),
|
||||
|
||||
position: 6,
|
||||
canBeHidden: true
|
||||
canBeHidden: true,
|
||||
visible: true,
|
||||
chainIds: "1,10"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -190,6 +239,8 @@ SplitView {
|
||||
? "Market data error!" : ""
|
||||
|
||||
model: assetsModel
|
||||
formatBalance: (balance, key) => root.format(balance, key)
|
||||
onSortRequested: (roleName, order) => assetsModel.sortBy(roleName, order)
|
||||
|
||||
onSendRequested: (key) =>logs.logEvent(`send requested: ${key}`)
|
||||
onReceiveRequested: (key) => logs.logEvent(`receive requested: ${key}`)
|
||||
|
||||
@@ -2,16 +2,17 @@ import QtCore
|
||||
import QtQuick
|
||||
import QtTest
|
||||
|
||||
import StatusQ
|
||||
import StatusQ.Models
|
||||
|
||||
import shared.views
|
||||
import AppLayouts.Wallet.panels
|
||||
import AppLayouts.Wallet.controls
|
||||
import utils
|
||||
|
||||
import StatusQ
|
||||
import StatusQ.Models
|
||||
import QtModelsToolkit
|
||||
import SortFilterProxyModel
|
||||
|
||||
|
||||
Item {
|
||||
id: root
|
||||
width: 600
|
||||
@@ -36,13 +37,218 @@ Item {
|
||||
"Wrapped Ether", "Status Test Token", "Ether", "Dai Stablecoin"
|
||||
]
|
||||
|
||||
ListModel {
|
||||
// Role-compatible stub for the terminal AssetsAdaptorModel: derives
|
||||
// isCommunity/marketBalance/change1DayFiat, filters to visible rows and
|
||||
// re-sorts in place via sortBy(roleName, order) — mirrors the Nim model.
|
||||
SortFilterProxyModel {
|
||||
id: assetsModel
|
||||
|
||||
function formatBalance(amount, symbol) {
|
||||
return amount.toLocaleCurrencyString(Qt.locale(), symbol)
|
||||
property int sortRoleOrder: Qt.DescendingOrder
|
||||
property string sortRoleName: "name"
|
||||
|
||||
function sortBy(roleName, order) {
|
||||
assetsModel.sortRoleName = roleName
|
||||
assetsModel.sortRoleOrder = order
|
||||
}
|
||||
|
||||
sourceModel: baseAssetsModel
|
||||
proxyRoles: [
|
||||
FastExpressionRole {
|
||||
name: "isCommunity"
|
||||
expression: !!model.communityId ? "community" : ""
|
||||
expectedRoles: ["communityId"]
|
||||
},
|
||||
FastExpressionRole {
|
||||
name: "marketBalance"
|
||||
expression: model.balance * model.marketPrice
|
||||
expectedRoles: ["balance", "marketPrice"]
|
||||
},
|
||||
FastExpressionRole {
|
||||
name: "change1DayFiat"
|
||||
expression: model.marketBalance * (1 - (1 / (model.marketChangePct24hour / 100 + 1)))
|
||||
expectedRoles: ["marketBalance", "marketChangePct24hour"]
|
||||
}
|
||||
]
|
||||
filters: ValueFilter { roleName: "visible"; value: true }
|
||||
sorters: [
|
||||
RoleSorter { roleName: "isCommunity" },
|
||||
RoleSorter {
|
||||
roleName: assetsModel.sortRoleName
|
||||
sortOrder: assetsModel.sortRoleOrder
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
ListModel {
|
||||
id: baseAssetsModel
|
||||
|
||||
Component.onCompleted: {
|
||||
append([
|
||||
{
|
||||
key: "key_DAI",
|
||||
symbol: "DAI",
|
||||
name: "Dai Stablecoin",
|
||||
logoUri: Constants.tokenIcon("DAI", false),
|
||||
balance: 1.0,
|
||||
balanceLoading: false,
|
||||
marketDetailsAvailable: true,
|
||||
marketDetailsLoading: false,
|
||||
marketPrice: 3.0,
|
||||
marketChangePct24hour: 5.0,
|
||||
communityId: "",
|
||||
communityName: "",
|
||||
communityImage: Qt.resolvedUrl(""),
|
||||
position: 1,
|
||||
canBeHidden: true,
|
||||
visible: true,
|
||||
chainIds: "1"
|
||||
},
|
||||
{
|
||||
key: "key_STT",
|
||||
symbol: "STT",
|
||||
name: "Status Test Token",
|
||||
logoUri: Constants.tokenIcon("STT", false),
|
||||
balance: 2.0,
|
||||
balanceLoading: false,
|
||||
marketDetailsAvailable: true,
|
||||
marketDetailsLoading: false,
|
||||
marketPrice: 2.0,
|
||||
marketChangePct24hour: 5.0,
|
||||
communityId: "",
|
||||
communityName: "",
|
||||
communityImage: Qt.resolvedUrl(""),
|
||||
position: 2,
|
||||
canBeHidden: true,
|
||||
visible: true,
|
||||
chainIds: "1"
|
||||
},
|
||||
{
|
||||
key: "key_WETH",
|
||||
symbol: "WETH",
|
||||
name: "Wrapped Ether",
|
||||
logoUri: Constants.tokenIcon("ETH", false),
|
||||
balance: 3.0,
|
||||
balanceLoading: false,
|
||||
marketDetailsAvailable: true,
|
||||
marketDetailsLoading: false,
|
||||
marketPrice: 3.1,
|
||||
marketChangePct24hour: 5.0,
|
||||
communityId: "",
|
||||
communityName: "",
|
||||
communityImage: Qt.resolvedUrl(""),
|
||||
position: 3,
|
||||
canBeHidden: true,
|
||||
visible: true,
|
||||
chainIds: "1"
|
||||
},
|
||||
{
|
||||
key: "key_ETH",
|
||||
symbol: "ETH",
|
||||
name: "Ether",
|
||||
logoUri: Constants.tokenIcon("ETH", false),
|
||||
balance: 4.0,
|
||||
balanceLoading: false,
|
||||
marketDetailsAvailable: true,
|
||||
marketDetailsLoading: false,
|
||||
marketPrice: 4.1,
|
||||
marketChangePct24hour: 5.0,
|
||||
communityId: "",
|
||||
communityName: "",
|
||||
communityImage: Qt.resolvedUrl(""),
|
||||
position: 4,
|
||||
canBeHidden: false,
|
||||
visible: true,
|
||||
chainIds: "1"
|
||||
}
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: assetsViewComponent
|
||||
AssetsView {
|
||||
width: root.width
|
||||
height: root.height
|
||||
sorterVisible: true
|
||||
model: assetsModel
|
||||
onSortRequested: (roleName, order) => assetsModel.sortBy(roleName, order)
|
||||
}
|
||||
}
|
||||
|
||||
// A source model that starts empty; rows are appended during the test to
|
||||
// reproduce the production timing where token rows arrive asynchronously,
|
||||
// after the view is already up in its `loading` state.
|
||||
ListModel {
|
||||
id: asyncBaseModel
|
||||
}
|
||||
|
||||
SortFilterProxyModel {
|
||||
id: asyncAssetsModel
|
||||
|
||||
property int sortRoleOrder: Qt.DescendingOrder
|
||||
property string sortRoleName: "name"
|
||||
|
||||
function sortBy(roleName, order) {
|
||||
asyncAssetsModel.sortRoleName = roleName
|
||||
asyncAssetsModel.sortRoleOrder = order
|
||||
}
|
||||
|
||||
sourceModel: asyncBaseModel
|
||||
proxyRoles: [
|
||||
FastExpressionRole {
|
||||
name: "isCommunity"
|
||||
expression: !!model.communityId ? "community" : ""
|
||||
expectedRoles: ["communityId"]
|
||||
},
|
||||
FastExpressionRole {
|
||||
name: "marketBalance"
|
||||
expression: model.balance * model.marketPrice
|
||||
expectedRoles: ["balance", "marketPrice"]
|
||||
},
|
||||
FastExpressionRole {
|
||||
name: "change1DayFiat"
|
||||
expression: model.marketBalance * (1 - (1 / (model.marketChangePct24hour / 100 + 1)))
|
||||
expectedRoles: ["marketBalance", "marketChangePct24hour"]
|
||||
}
|
||||
]
|
||||
filters: ValueFilter { roleName: "visible"; value: true }
|
||||
sorters: [
|
||||
RoleSorter { roleName: "isCommunity" },
|
||||
RoleSorter {
|
||||
roleName: asyncAssetsModel.sortRoleName
|
||||
sortOrder: asyncAssetsModel.sortRoleOrder
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Component {
|
||||
id: asyncAssetsViewComponent
|
||||
AssetsView {
|
||||
width: root.width
|
||||
height: root.height
|
||||
sorterVisible: true
|
||||
model: asyncAssetsModel
|
||||
onSortRequested: (roleName, order) => asyncAssetsModel.sortBy(roleName, order)
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: modelChangedSpyComponent
|
||||
SignalSpy { signalName: "modelChanged" }
|
||||
}
|
||||
|
||||
// ----- Custom-ordering harness -------------------------------------------
|
||||
// Ported from the upstream custom-ordering tests (commit 50c24691dc) and
|
||||
// adapted to the terminal-model architecture: our AssetsView no longer
|
||||
// sorts internally, it emits sortRequested(roleName, order) and the
|
||||
// consumer sorts the model. The harness therefore wires onSortRequested to
|
||||
// a sortBy stub (mirroring RootStore.walletAssetsStore.sortAssets in
|
||||
// production) and gives assetsViewModel a RoleSorter driven by the
|
||||
// requested role. The "position" role is supplied by the controller, so a
|
||||
// TokenOrderCustom sort reorders the wallet list into the saved order.
|
||||
ListModel {
|
||||
id: customOrderAssetsModel
|
||||
|
||||
function marketDetailsForPrice(price) {
|
||||
return {
|
||||
currencyPrice: {
|
||||
@@ -62,7 +268,6 @@ Item {
|
||||
name: "Dai Stablecoin",
|
||||
logoUri: Constants.tokenIcon("DAI", false),
|
||||
balance: 1.0,
|
||||
balanceText: formatBalance(1.0, "DAI"),
|
||||
balanceLoading: false,
|
||||
error: "",
|
||||
decimals: 18,
|
||||
@@ -73,8 +278,10 @@ Item {
|
||||
marketChangePct24hour: 5.0,
|
||||
communityId: "",
|
||||
communityName: "",
|
||||
communityIcon: Qt.resolvedUrl(""),
|
||||
canBeHidden: true
|
||||
communityImage: Qt.resolvedUrl(""),
|
||||
canBeHidden: true,
|
||||
visible: true,
|
||||
chainIds: "1"
|
||||
},
|
||||
{
|
||||
key: "key_STT",
|
||||
@@ -82,7 +289,6 @@ Item {
|
||||
name: "Status Test Token",
|
||||
logoUri: Constants.tokenIcon("STT", false),
|
||||
balance: 2.0,
|
||||
balanceText: formatBalance(2.0, "STT"),
|
||||
balanceLoading: false,
|
||||
error: "",
|
||||
decimals: 18,
|
||||
@@ -93,8 +299,10 @@ Item {
|
||||
marketChangePct24hour: 5.0,
|
||||
communityId: "",
|
||||
communityName: "",
|
||||
communityIcon: Qt.resolvedUrl(""),
|
||||
canBeHidden: true
|
||||
communityImage: Qt.resolvedUrl(""),
|
||||
canBeHidden: true,
|
||||
visible: true,
|
||||
chainIds: "1"
|
||||
},
|
||||
{
|
||||
key: "key_WETH",
|
||||
@@ -102,7 +310,6 @@ Item {
|
||||
name: "Wrapped Ether",
|
||||
logoUri: Constants.tokenIcon("ETH", false),
|
||||
balance: 3.0,
|
||||
balanceText: formatBalance(3.0, "WETH"),
|
||||
balanceLoading: false,
|
||||
error: "",
|
||||
decimals: 18,
|
||||
@@ -113,8 +320,10 @@ Item {
|
||||
marketChangePct24hour: 5.0,
|
||||
communityId: "",
|
||||
communityName: "",
|
||||
communityIcon: Qt.resolvedUrl(""),
|
||||
canBeHidden: true
|
||||
communityImage: Qt.resolvedUrl(""),
|
||||
canBeHidden: true,
|
||||
visible: true,
|
||||
chainIds: "1"
|
||||
},
|
||||
{
|
||||
key: "key_ETH",
|
||||
@@ -122,7 +331,6 @@ Item {
|
||||
name: "Ether",
|
||||
logoUri: Constants.tokenIcon("ETH", false),
|
||||
balance: 4.0,
|
||||
balanceText: formatBalance(4.0, "ETH"),
|
||||
balanceLoading: false,
|
||||
error: "",
|
||||
decimals: 18,
|
||||
@@ -133,8 +341,10 @@ Item {
|
||||
marketChangePct24hour: 5.0,
|
||||
communityId: "",
|
||||
communityName: "",
|
||||
communityIcon: Qt.resolvedUrl(""),
|
||||
canBeHidden: false
|
||||
communityImage: Qt.resolvedUrl(""),
|
||||
canBeHidden: false,
|
||||
visible: true,
|
||||
chainIds: "1"
|
||||
}
|
||||
])
|
||||
}
|
||||
@@ -161,7 +371,7 @@ Item {
|
||||
|
||||
ManageTokensController {
|
||||
id: assetsController
|
||||
sourceModel: assetsModel
|
||||
sourceModel: customOrderAssetsModel
|
||||
settingsKey: "AssetsViewCustomOrderTest"
|
||||
serializeAsCollectibles: false
|
||||
|
||||
@@ -179,7 +389,18 @@ Item {
|
||||
|
||||
SortFilterProxyModel {
|
||||
id: assetsViewModel
|
||||
sourceModel: assetsModel
|
||||
|
||||
// Terminal-model adaptation: our AssetsView asks the consumer to sort;
|
||||
// sortBy(roleName, order) re-sorts this proxy in place.
|
||||
property int sortRoleOrder: Qt.DescendingOrder
|
||||
property string sortRoleName: "balance"
|
||||
|
||||
function sortBy(roleName, order) {
|
||||
assetsViewModel.sortRoleName = roleName
|
||||
assetsViewModel.sortRoleOrder = order
|
||||
}
|
||||
|
||||
sourceModel: customOrderAssetsModel
|
||||
proxyRoles: [
|
||||
FastExpressionRole {
|
||||
name: "position"
|
||||
@@ -190,6 +411,12 @@ Item {
|
||||
expectedRoles: ["key"]
|
||||
}
|
||||
]
|
||||
sorters: [
|
||||
RoleSorter {
|
||||
roleName: assetsViewModel.sortRoleName
|
||||
sortOrder: assetsViewModel.sortRoleOrder
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
QtObject {
|
||||
@@ -198,16 +425,6 @@ Item {
|
||||
property string screen: "settings"
|
||||
}
|
||||
|
||||
Component {
|
||||
id: assetsViewComponent
|
||||
AssetsView {
|
||||
width: root.width
|
||||
height: root.height
|
||||
sorterVisible: true
|
||||
model: assetsModel
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: customOrderingHarnessComponent
|
||||
Item {
|
||||
@@ -224,6 +441,7 @@ Item {
|
||||
model: assetsViewModel
|
||||
customOrderAvailable: assetsController.hasSettings
|
||||
onManageTokensRequested: flowState.screen = "manageTokens"
|
||||
onSortRequested: (roleName, order) => assetsViewModel.sortBy(roleName, order)
|
||||
|
||||
function refreshSortSettings() {
|
||||
let value = SortOrderComboBox.TokenOrderBalance
|
||||
@@ -368,6 +586,100 @@ Item {
|
||||
function test_sortByUi_asc_desc(data) {
|
||||
verifySortByUi(data.optionText, data.orderAsc, data.orderDesc)
|
||||
}
|
||||
|
||||
// A periodic market/balance refresh toggles `loading` while real data is
|
||||
// already present. Once the regular model has content the list must stay
|
||||
// bound to it — swapping to the loading placeholder tears down and
|
||||
// recreates every delegate on each refresh.
|
||||
function test_loadingToggle_keepsPopulatedModel() {
|
||||
const listView = getListView(controlUnderTest)
|
||||
waitForRendering(listView)
|
||||
compare(listView.count, 4)
|
||||
const modelInstance = listView.model
|
||||
|
||||
controlUnderTest.loading = true
|
||||
verify(listView.model === modelInstance,
|
||||
"loading placeholder must not replace the populated model on refresh")
|
||||
compare(listView.count, 4)
|
||||
|
||||
controlUnderTest.loading = false
|
||||
verify(listView.model === modelInstance)
|
||||
compare(listView.count, 4)
|
||||
}
|
||||
|
||||
// Once data is present, toggling `loading` must not re-assign the list's
|
||||
// model at all. Keeping the same model instance is not enough: on the
|
||||
// production ListView, re-assigning even the identical DelegateModel makes
|
||||
// the view rebuild every delegate. The model binding must therefore drop
|
||||
// `loading` from its dependencies once the content latch is set, so the
|
||||
// periodic refresh toggles never re-evaluate it.
|
||||
function test_loadingToggle_doesNotReassignModel() {
|
||||
const listView = getListView(controlUnderTest)
|
||||
waitForRendering(listView)
|
||||
compare(listView.count, 4) // regular model, data present
|
||||
|
||||
const spy = modelChangedSpyComponent.createObject(root, { target: listView })
|
||||
verify(spy.valid)
|
||||
spy.clear()
|
||||
|
||||
for (let i = 0; i < 4; ++i) {
|
||||
controlUnderTest.loading = (i % 2 === 0)
|
||||
waitForRendering(listView)
|
||||
}
|
||||
|
||||
compare(spy.count, 0,
|
||||
"loading toggles must not re-assign the list model once data is present")
|
||||
spy.destroy()
|
||||
}
|
||||
|
||||
// Covers the production initial condition the sort test above does not:
|
||||
// the view starts in the global `loading` state (list on the placeholder)
|
||||
// and token rows arrive asynchronously afterwards. Once real data has
|
||||
// arrived the list must switch to and stay on the regular model across
|
||||
// the periodic `loading` toggles, never rebuilding against the
|
||||
// placeholder. NOTE: the on-device regression this guards is driven by
|
||||
// the C++ terminal model not reporting its rows until a view consumes
|
||||
// the regular DelegateModel; QML ListModel/SortFilterProxyModel report
|
||||
// rows eagerly, so this case cannot fully reproduce that timing here —
|
||||
// the authoritative red/green for it is the on-device startup trace.
|
||||
function test_loadingStartTrue_latchesFromSourceModel() {
|
||||
asyncBaseModel.clear()
|
||||
const view = createTemporaryObject(asyncAssetsViewComponent, root,
|
||||
{ loading: true })
|
||||
verify(!!view)
|
||||
const listView = getListView(view)
|
||||
waitForRendering(listView)
|
||||
|
||||
// Rows arrive while still loading and while the list is on the
|
||||
// placeholder (the regular DelegateModel is not consumed yet).
|
||||
asyncBaseModel.append({
|
||||
key: "key_ETH", symbol: "ETH", name: "Ether",
|
||||
logoUri: Constants.tokenIcon("ETH", false),
|
||||
balance: 4.0, balanceLoading: false,
|
||||
marketDetailsAvailable: true, marketDetailsLoading: false,
|
||||
marketPrice: 4.1, marketChangePct24hour: 5.0,
|
||||
communityId: "", communityName: "",
|
||||
communityImage: Qt.resolvedUrl(""),
|
||||
position: 1, canBeHidden: false, visible: true, chainIds: "1"
|
||||
})
|
||||
waitForRendering(listView)
|
||||
|
||||
// First real data has arrived: switch to the regular model and stay
|
||||
// there across the periodic loading toggle.
|
||||
view.loading = false
|
||||
waitForRendering(listView)
|
||||
const populatedModel = listView.model
|
||||
compare(listView.count, 1)
|
||||
|
||||
view.loading = true
|
||||
verify(listView.model === populatedModel,
|
||||
"loading placeholder must not replace the populated model after first data")
|
||||
compare(listView.count, 1)
|
||||
|
||||
view.loading = false
|
||||
verify(listView.model === populatedModel)
|
||||
compare(listView.count, 1)
|
||||
}
|
||||
}
|
||||
|
||||
TestCase {
|
||||
|
||||
@@ -32,6 +32,16 @@ QtObject {
|
||||
*/
|
||||
readonly property var baseGroupedAccountAssetModel: walletSectionAssets.groupedAccountAssetsModel
|
||||
|
||||
// Terminal, already-aggregated assets model built in Nim (replaces the QML
|
||||
// AssetsViewAdaptor proxy chain). Consumed by AssetsView in RightTabView.
|
||||
readonly property var assetsModel: walletSectionAssetsView.assetsModel
|
||||
|
||||
// Re-sorts the terminal assets model in place. AssetsView emits the sort
|
||||
// intent; this wires it to the model.
|
||||
function sortAssets(roleName, order) {
|
||||
walletSectionAssetsView.sortBy(roleName, order)
|
||||
}
|
||||
|
||||
readonly property var assetsController: ManageTokensController {
|
||||
sourceModel: groupedAccountAssetsModel
|
||||
settingsKey: "WalletAssets"
|
||||
|
||||
@@ -305,39 +305,19 @@ RightTabBaseView {
|
||||
AssetsView {
|
||||
id: walletAssetsView
|
||||
|
||||
AssetsViewAdaptor {
|
||||
id: assetsViewAdaptor
|
||||
|
||||
accounts: RootStore.addressFilters
|
||||
chains: root.networksStore.networkFilters
|
||||
|
||||
marketValueThreshold:
|
||||
RootStore.tokensStore.displayAssetsBelowBalance
|
||||
? RootStore.tokensStore.getDisplayAssetsBelowBalanceThresholdDisplayAmount()
|
||||
: 0
|
||||
|
||||
Connections {
|
||||
target: RootStore.tokensStore
|
||||
|
||||
function displayAssetsBelowBalanceThresholdChanged() {
|
||||
assetsViewAdaptor.marketValueThresholdChanged()
|
||||
}
|
||||
}
|
||||
|
||||
tokensModel: RootStore.walletAssetsStore.groupedAccountAssetsModel
|
||||
|
||||
formatBalance: (balance, key) => {
|
||||
return LocaleUtils.currencyAmountToLocaleString(
|
||||
RootStore.currencyStore.getCurrencyAmount(balance, key))
|
||||
}
|
||||
|
||||
chainsError: (chains) => {
|
||||
if (!root.networkConnectionStore)
|
||||
return ""
|
||||
return root.networkConnectionStore.getBlockchainNetworkDownText(chains)
|
||||
}
|
||||
formatBalance: (balance, key) => {
|
||||
return LocaleUtils.currencyAmountToLocaleString(
|
||||
RootStore.currencyStore.getCurrencyAmount(balance, key))
|
||||
}
|
||||
|
||||
chainsError: (chains) => {
|
||||
if (!root.networkConnectionStore)
|
||||
return ""
|
||||
return root.networkConnectionStore.getBlockchainNetworkDownText(chains)
|
||||
}
|
||||
|
||||
onSortRequested: (roleName, order) => RootStore.walletAssetsStore.sortAssets(roleName, order)
|
||||
|
||||
function refreshSortSettings() {
|
||||
settings.category = settingsCategoryName
|
||||
walletSettings.sync()
|
||||
@@ -387,7 +367,7 @@ RightTabBaseView {
|
||||
loading: RootStore.overview.balanceLoading
|
||||
sorterVisible: filterButton.checked
|
||||
customOrderAvailable: RootStore.walletAssetsStore.assetsController.hasSettings
|
||||
model: assetsViewAdaptor.model
|
||||
model: RootStore.walletAssetsStore.assetsModel
|
||||
bannerComponent: buyReceiveBannerComponent
|
||||
|
||||
marketDataError: !!root.networkConnectionStore
|
||||
@@ -422,15 +402,15 @@ RightTabBaseView {
|
||||
onCommunityClicked: Global.switchToCommunity(communityKey)
|
||||
|
||||
onHideRequested: (key) => {
|
||||
const token = SQUtils.ModelUtils.getByKey(model, "key", key)
|
||||
Global.openConfirmHideAssetPopup(token.symbol, token.name, token.icon, !!token.communityId)
|
||||
const token = SQUtils.ModelUtils.getByKey(RootStore.walletAssetsStore.groupedAccountAssetsModel, "key", key)
|
||||
Global.openConfirmHideAssetPopup(token.symbol, token.name, token.logoUri, !!token.communityId)
|
||||
}
|
||||
onHideCommunityAssetsRequested:
|
||||
(communityKey) => {
|
||||
const community = SQUtils.ModelUtils.getByKey(model, "communityId", communityKey)
|
||||
const community = SQUtils.ModelUtils.getByKey(RootStore.walletAssetsStore.groupedAccountAssetsModel, "communityId", communityKey)
|
||||
confirmHideCommunityAssetsPopup.createObject(root, {
|
||||
name: community.communityName,
|
||||
icon: community.communityIcon,
|
||||
icon: community.communityImage,
|
||||
communityId: communityKey }
|
||||
).open()
|
||||
}
|
||||
@@ -439,7 +419,7 @@ RightTabBaseView {
|
||||
Constants.settingsSubsection.wallet,
|
||||
Constants.walletSettingsSubsection.manageAssets)
|
||||
onAssetClicked: (key) => {
|
||||
const tokenGroup = SQUtils.ModelUtils.getByKey(model, "key", key)
|
||||
const tokenGroup = SQUtils.ModelUtils.getByKey(RootStore.walletAssetsStore.groupedAccountAssetsModel, "key", key)
|
||||
|
||||
assetDetailView.tokenGroup = tokenGroup
|
||||
RootStore.setCurrentViewedHolding(tokenGroup.key, Constants.TokenType.ERC20, tokenGroup.communityId ?? "")
|
||||
|
||||
@@ -4,6 +4,8 @@ import QtQuick.Layouts
|
||||
|
||||
import QtQml.Models
|
||||
|
||||
import QtModelsToolkit
|
||||
|
||||
import StatusQ
|
||||
import StatusQ.Core
|
||||
import StatusQ.Core.Theme
|
||||
@@ -14,13 +16,18 @@ import shared.controls
|
||||
import shared.popups
|
||||
import utils
|
||||
|
||||
import SortFilterProxyModel
|
||||
|
||||
|
||||
Control {
|
||||
id: root
|
||||
|
||||
/**
|
||||
Model contract (terminal model — already visible-filtered and sorted; no
|
||||
proxy is placed above it here):
|
||||
- provides the derived roles `isCommunity`, `marketBalance`,
|
||||
`change1DayFiat` and a `chainIds` role (comma-separated chain ids)
|
||||
in addition to the base token roles below;
|
||||
- re-sorts in place via `sortBy(roleName, order)` — this view emits the
|
||||
`sortRequested` intent, the consumer wires it to the model.
|
||||
|
||||
Expected model structure:
|
||||
|
||||
key [string] - refers to token group key
|
||||
@@ -118,6 +125,17 @@ Control {
|
||||
// formatting function for fiat currency values
|
||||
property var formatFiat: balance => `${balance.toLocaleCurrencyString(Qt.locale())}`
|
||||
|
||||
// formats a token's balance for display (moved out of the retired proxy chain;
|
||||
// the consumer supplies the currency context)
|
||||
property var formatBalance: (balance, key) => `${balance.toLocaleString(Qt.locale())} ${key}`
|
||||
|
||||
// returns an error message for a token given its contributing chain ids,
|
||||
// or an empty string when there is none
|
||||
property var chainsError: (chainIds) => ""
|
||||
|
||||
// sort intent — consumer wires this to the model's sortBy(roleName, order)
|
||||
signal sortRequested(string roleName, int order)
|
||||
|
||||
signal sendRequested(string key)
|
||||
signal receiveRequested(string key)
|
||||
signal swapRequested(string key)
|
||||
@@ -149,41 +167,33 @@ Control {
|
||||
readonly property int loadingItemsCount: 25
|
||||
property int sortOrder: Qt.DescendingOrder
|
||||
property int sortValue: -1
|
||||
|
||||
// Latched true once the source model has ever had rows. Keeps the list
|
||||
// bound to the real model across periodic refreshes that toggle
|
||||
// `loading`; the placeholder only ever shows before the first real data.
|
||||
property bool everHadContent: false
|
||||
// Latch off the source model's row count. Guard the null case with `&&`
|
||||
// rather than optional chaining: the AOT-compiled mobile build drops the
|
||||
// reactive dependency captured through `?.`, so the latch would never
|
||||
// re-evaluate when rows arrive after creation.
|
||||
readonly property int modelCount: !!root.model ? root.model.ModelCount.count : 0
|
||||
onModelCountChanged: if (modelCount > 0) everHadContent = true
|
||||
Component.onCompleted: if (modelCount > 0) everHadContent = true
|
||||
|
||||
// Emit the current sorter selection as an intent; separators carry an
|
||||
// empty role name and are ignored.
|
||||
function requestSort() {
|
||||
const roleName = sortOrderComboBox.currentSortRoleName
|
||||
if (!roleName)
|
||||
return
|
||||
root.sortRequested(roleName, sortOrderComboBox.currentSortOrder)
|
||||
}
|
||||
}
|
||||
|
||||
SortFilterProxyModel {
|
||||
id: sfpm
|
||||
|
||||
sourceModel: root.model ?? null
|
||||
|
||||
proxyRoles: [
|
||||
// helper role for rendering section delegate
|
||||
FastExpressionRole {
|
||||
name: "isCommunity"
|
||||
expression: !!model.communityId ? "community" : ""
|
||||
expectedRoles: ["communityId"]
|
||||
},
|
||||
FastExpressionRole {
|
||||
name: "marketBalance"
|
||||
expression: model.balance * model.marketPrice
|
||||
expectedRoles: ["balance", "marketPrice"]
|
||||
},
|
||||
FastExpressionRole {
|
||||
name: "change1DayFiat"
|
||||
expression: model.marketBalance * (1 - (1 / (model.marketChangePct24hour / 100 + 1)))
|
||||
expectedRoles: ["marketBalance", "marketChangePct24hour"]
|
||||
}
|
||||
]
|
||||
|
||||
sorters: [
|
||||
RoleSorter {
|
||||
roleName: "isCommunity"
|
||||
},
|
||||
RoleSorter {
|
||||
roleName: sortOrderComboBox.currentSortRoleName
|
||||
sortOrder: sortOrderComboBox.currentSortOrder
|
||||
}
|
||||
]
|
||||
Connections {
|
||||
target: sortOrderComboBox
|
||||
function onCurrentSortRoleNameChanged() { d.requestSort() }
|
||||
function onCurrentSortOrderChanged() { d.requestSort() }
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
@@ -273,16 +283,22 @@ Control {
|
||||
DelegateModel {
|
||||
id: regularModel
|
||||
|
||||
model: sfpm
|
||||
model: root.model ?? null
|
||||
|
||||
delegate: TokenDelegate {
|
||||
objectName: `AssetView_TokenListItem_${model.symbol}` // TODO: use model.key
|
||||
|
||||
width: ListView.view.width
|
||||
|
||||
// chainIds arrives as a comma-separated string from the terminal model
|
||||
readonly property var chainIdsList: {
|
||||
const ids = model.chainIds
|
||||
return (ids && ids.length) ? ids.split(",").map(Number) : []
|
||||
}
|
||||
|
||||
name: model.name
|
||||
icon: model.logoUri
|
||||
balance: model.balanceText
|
||||
balance: root.formatBalance(model.balance, model.key)
|
||||
balanceLoading: model.balanceLoading
|
||||
marketBalance: root.formatFiat(model.marketBalance)
|
||||
|
||||
@@ -295,7 +311,7 @@ Control {
|
||||
communityName: model.communityName ?? ""
|
||||
communityIcon: model.communityImage ?? ""
|
||||
|
||||
errorTooltipText_1: model.error
|
||||
errorTooltipText_1: root.chainsError(chainIdsList)
|
||||
errorTooltipText_2: root.marketDataError
|
||||
|
||||
errorMode: !!root.balanceError
|
||||
@@ -332,7 +348,14 @@ Control {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
model: root.loading ? loadingModel : regularModel
|
||||
// Operand order matters: once `everHadContent` latches true,
|
||||
// `!d.everHadContent` is false and short-circuits `&&`, dropping
|
||||
// `root.loading` from this binding's captured dependencies. The
|
||||
// periodic `loading` toggles then no longer re-evaluate and re-assign
|
||||
// the model — re-assigning even the same DelegateModel makes the view
|
||||
// rebuild every delegate. Before first data it still shows the
|
||||
// placeholder while `loading` is true.
|
||||
model: (!d.everHadContent && root.loading) ? loadingModel : regularModel
|
||||
|
||||
section {
|
||||
property: "isCommunity"
|
||||
|
||||
@@ -1,222 +0,0 @@
|
||||
import QtQml
|
||||
|
||||
import StatusQ
|
||||
import StatusQ.Core.Utils
|
||||
|
||||
import utils
|
||||
|
||||
import QtModelsToolkit
|
||||
import SortFilterProxyModel
|
||||
|
||||
QObject {
|
||||
id: root
|
||||
|
||||
/**
|
||||
Expected model structure:
|
||||
|
||||
Tokens related part:
|
||||
|
||||
tokensKey [string] - unique identifier of a token, e.g "0x3234235"
|
||||
symbol [string] - token's symbol e.g. "ETH" or "SNT"
|
||||
name [string] - token's name e.g. "Ether" or "Dai"
|
||||
image [url] - token's icon for custom tokens
|
||||
decimals [int] - number of decimal places, e.g. 18 for ETH
|
||||
balances [model] - submodel of balances per chain/account
|
||||
chainId [int] - unique identifier of a chain
|
||||
account [string] - unique identifier of an account
|
||||
balance [string] - balance in basic unit as big integer string
|
||||
marketDetails [object] - object holding market details
|
||||
changePct24hour [double] - percentage change of fiat price in last day
|
||||
currencyPrice [object] - object holding fiat price details
|
||||
amount [double] - fiat prace of 1 logical unit of cryptocurrency
|
||||
detailsLoading [bool] - indicatator if market details are ready to use
|
||||
position [int] - custom order position
|
||||
visible [bool] - token management visiblity flag
|
||||
|
||||
Community related part (relevant for community minted assets, empty otherwise):
|
||||
|
||||
communityId [string] - unique identifier of a community, e.g. "0x6734235"
|
||||
communityName [string] - name of a community e.g. "Crypto Kitties"
|
||||
communityImage [url] - community's icon url
|
||||
**/
|
||||
property var tokensModel
|
||||
|
||||
// function formatting tokens balance expressed in a commonly used units,
|
||||
// e.g. 1.2 for 1.2 ETH, according to rules specific for given symbol
|
||||
property var formatBalance:
|
||||
(balance, key) => `${balance.toLocaleString(Qt.locale())} ${key}`
|
||||
|
||||
// function providing error message per token depending on used chains,
|
||||
// should return empty string if no error found
|
||||
property var chainsError: chains => ""
|
||||
|
||||
// array[Number]list of chain identifiers used for balance calculation
|
||||
property var chains: []
|
||||
|
||||
// list of accounts used for balance calculation
|
||||
property var accounts: []
|
||||
|
||||
// threshold below which the token is omitted from the output model
|
||||
property double marketValueThreshold
|
||||
|
||||
/**
|
||||
Model structure:
|
||||
|
||||
All roles from the source model are passed directly to the output model,
|
||||
additionally:
|
||||
|
||||
key [string] - refers to token group key
|
||||
icon [url] - from image or fetched by symbol for well-known tokens
|
||||
balance [double] - tokens balance is the commonly used unit, e.g. 1.2 for 1.2 ETH,
|
||||
computed from balances according to provided criteria
|
||||
balanceText [string] - formatted and localized balance
|
||||
balanceLoading [bool] - true while any per-(chain, account) balance matching the
|
||||
active filter has not been fetched yet from status-go
|
||||
error [string] - error message related to balance
|
||||
|
||||
marketDetailsAvailable [bool] - specifies if market datails are available for given token
|
||||
marketDetailsLoading [bool] - specifies if market datails are available for given token
|
||||
marketPrice [double] - specifies market price in currently used currency
|
||||
marketChangePct24hour [double] - percentage price change in last 24 hours, e.g. 0.5 for 0.5% of price change
|
||||
|
||||
canBeHidden [bool] - specifies if given token can be hidden (e.g. ETH should be always visible)
|
||||
|
||||
communityIcon [url] - renamed from communityImage
|
||||
**/
|
||||
readonly property alias model: sfpm
|
||||
|
||||
ObjectProxyModel {
|
||||
id: proxyModel
|
||||
|
||||
objectName: "assetsViewAdaptorProxyModel"
|
||||
|
||||
sourceModel: root.tokensModel ?? null
|
||||
|
||||
delegate: QObject {
|
||||
readonly property var rootModel: model
|
||||
readonly property bool hasCommunityId: !!model.communityId
|
||||
readonly property var marketDetails: model.marketDetails
|
||||
|
||||
// Read-only roles exposed to the model:
|
||||
|
||||
readonly property string error:
|
||||
root.chainsError(chainsAggregator.uniqueChains)
|
||||
|
||||
readonly property double balance:
|
||||
AmountsArithmetic.toNumber(totalBalanceAggregator.value, model.decimals)
|
||||
readonly property string balanceText: root.formatBalance(balance, model.key)
|
||||
readonly property bool balanceLoading: loadingBalancesAggregator.value > 0
|
||||
|
||||
readonly property bool marketDetailsAvailable: !hasCommunityId
|
||||
readonly property bool marketDetailsLoading: model.detailsLoading
|
||||
readonly property real marketPrice: marketDetails?.currencyPrice?.amount ?? 0
|
||||
readonly property real marketChangePct24hour: marketDetails?.changePct24hour ?? 0
|
||||
|
||||
readonly property bool visible: {
|
||||
if (!model.visible)
|
||||
return false
|
||||
|
||||
if (filteredBalances.ModelCount.empty)
|
||||
return false
|
||||
|
||||
if (hasCommunityId)
|
||||
return true
|
||||
|
||||
// Keep mandatory/known tokens visible while their balances are still being fetched,
|
||||
// so the user sees them in a loading state instead of having an empty Assets tab.
|
||||
if (balanceLoading)
|
||||
return true
|
||||
|
||||
return balance * marketPrice >= root.marketValueThreshold
|
||||
}
|
||||
|
||||
readonly property url icon:
|
||||
!!model.logoUri ? model.logoUri
|
||||
: Constants.tokenIcon(model.symbol, false)
|
||||
|
||||
readonly property url communityIcon: model.communityImage ?? ""
|
||||
|
||||
readonly property bool canBeHidden: {
|
||||
for (const chain of root.chains) {
|
||||
if (model.symbol === Utils.getNativeTokenSymbol(chain)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
SortFilterProxyModel {
|
||||
id: filteredBalances
|
||||
|
||||
sourceModel: rootModel.balances
|
||||
|
||||
filters: [
|
||||
OneOfFilter {
|
||||
roleName: "chainId"
|
||||
array: root.chains
|
||||
separator: ":"
|
||||
},
|
||||
OneOfFilter {
|
||||
roleName: "account"
|
||||
array: root.accounts
|
||||
separator: ":"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
FunctionAggregator {
|
||||
id: totalBalanceAggregator
|
||||
|
||||
model: filteredBalances
|
||||
initialValue: "0"
|
||||
roleName: "balance"
|
||||
|
||||
aggregateFunction: (aggr, value) => AmountsArithmetic.sum(
|
||||
AmountsArithmetic.fromString(aggr),
|
||||
AmountsArithmetic.fromString(value)).toString()
|
||||
}
|
||||
|
||||
FunctionAggregator {
|
||||
id: chainsAggregator
|
||||
|
||||
readonly property var uniqueChains: [...new Set(value).values()]
|
||||
|
||||
model: filteredBalances
|
||||
initialValue: []
|
||||
roleName: "chainId"
|
||||
|
||||
aggregateFunction: (aggr, value) => [...aggr, value]
|
||||
}
|
||||
|
||||
FunctionAggregator {
|
||||
id: loadingBalancesAggregator
|
||||
|
||||
model: filteredBalances
|
||||
initialValue: 0
|
||||
roleName: "loading"
|
||||
|
||||
aggregateFunction: (aggr, value) => aggr + (value ? 1 : 0)
|
||||
}
|
||||
}
|
||||
|
||||
expectedRoles:
|
||||
["key", "symbol", "logoUri", "balances", "decimals",
|
||||
"detailsLoading", "marketDetails", "communityId", "communityImage",
|
||||
"visible"]
|
||||
exposedRoles:
|
||||
["error", "balance", "balanceText", "balanceLoading", "icon",
|
||||
"visible", "canBeHidden", "marketDetailsAvailable", "marketDetailsLoading",
|
||||
"marketPrice", "marketChangePct24hour", "communityIcon"]
|
||||
}
|
||||
|
||||
SortFilterProxyModel {
|
||||
id: sfpm
|
||||
|
||||
sourceModel: proxyModel
|
||||
|
||||
filters: ValueFilter {
|
||||
roleName: "visible"
|
||||
value: true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
AssetContextMenu 1.0 AssetContextMenu.qml
|
||||
AssetsView 1.0 AssetsView.qml
|
||||
AssetsViewAdaptor 1.0 AssetsViewAdaptor.qml
|
||||
ConfirmHideAssetPopup 1.0 ConfirmHideAssetPopup.qml
|
||||
ConfirmHideCommunityAssetsPopup 1.0 ConfirmHideCommunityAssetsPopup.qml
|
||||
ExistingContacts 1.0 ExistingContacts.qml
|
||||
|
||||
Reference in New Issue
Block a user