From b12caa3f9a663c38713239b47a5d5df769f1b063 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Cie=C5=9Blak?= Date: Thu, 13 Jun 2024 14:17:33 +0200 Subject: [PATCH] feat(Wallet): AssetsViewAdaptor added for preparing data for AssetsView --- storybook/pages/AssetsViewAdaptorPage.qml | 275 ++++++++++++++++++ .../Wallet/stores/WalletAssetsStore.qml | 2 + ui/imports/shared/views/AssetsViewAdaptor.qml | 202 +++++++++++++ ui/imports/shared/views/qmldir | 1 + 4 files changed, 480 insertions(+) create mode 100644 storybook/pages/AssetsViewAdaptorPage.qml create mode 100644 ui/imports/shared/views/AssetsViewAdaptor.qml diff --git a/storybook/pages/AssetsViewAdaptorPage.qml b/storybook/pages/AssetsViewAdaptorPage.qml new file mode 100644 index 0000000000..405a0e4a7c --- /dev/null +++ b/storybook/pages/AssetsViewAdaptorPage.qml @@ -0,0 +1,275 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import StatusQ.Models 0.1 + +import Storybook 1.0 + +import utils 1.0 +import shared.views 1.0 + +Item { + id: root + + ListModel { + id: listModel + + readonly property var data: [ + { + tokensKey: "key_ETH", + 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: Qt.resolvedUrl(""), + marketDetails: { + changePct24hour: -2.1232, + currencyPrice: { + amount: 3423.23898 + } + }, + detailsLoading: false, + image: Qt.resolvedUrl("") + }, + { + tokensKey: "key_SNT", + 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: Qt.resolvedUrl(""), + marketDetails: { + changePct24hour: 9.232, + currencyPrice: { + amount: 33.23898 + } + }, + detailsLoading: false, + image: Qt.resolvedUrl("") + }, + { + tokensKey: "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, + image: Constants.tokenIcon("ZRX", false) + } + ] + + 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()] + } + } + + ManageTokensController { + id: manageTokensController + + sourceModel: listModel + serializeAsCollectibles: false + + onRequestLoadSettings: { + loadingStarted() + + const jsonData = [ + { + "key": "ETH", + "position": 1, + "visible": true + }, + { + "key": "SNT", + "position": 2, + "visible": true + }, + { + "key": "MYASST", + "position": 5, + "visible": true + } + ] + + loadingFinished(JSON.stringify(jsonData)) + } + } + + AssetsViewAdaptor { + id: adaptor + + controller: manageTokensController + + 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 diff --git a/ui/app/AppLayouts/Wallet/stores/WalletAssetsStore.qml b/ui/app/AppLayouts/Wallet/stores/WalletAssetsStore.qml index e0358779ad..8c284174a6 100644 --- a/ui/app/AppLayouts/Wallet/stores/WalletAssetsStore.qml +++ b/ui/app/AppLayouts/Wallet/stores/WalletAssetsStore.qml @@ -95,6 +95,8 @@ QtObject { /* This model joins the "Tokens by symbol model combined with Community details" and "Grouped Account Assets Model" by tokenskey */ property LeftJoinModel groupedAccountAssetsModel: LeftJoinModel { + objectName: "groupedAccountAssetsModel" + leftModel: root.baseGroupedAccountAssetModel rightModel: _jointTokensBySymbolModel joinRole: "tokensKey" diff --git a/ui/imports/shared/views/AssetsViewAdaptor.qml b/ui/imports/shared/views/AssetsViewAdaptor.qml new file mode 100644 index 0000000000..18ab718de7 --- /dev/null +++ b/ui/imports/shared/views/AssetsViewAdaptor.qml @@ -0,0 +1,202 @@ +import QtQml 2.15 + +import StatusQ 0.1 +import StatusQ.Models 0.1 +import StatusQ.Core.Utils 0.1 + +import utils 1.0 + +import SortFilterProxyModel 0.2 + + +QObject { + id: root + + // Controller providing information about visibility and order defined + // by a user (token management) + required property ManageTokensController controller + + /** + 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 [string] - 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 + + 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, symbol) => `${balance.toLocaleString(Qt.locale())} ${symbol}` + + // function providing error message per token depending on used chains, + // should return empty string if no error found + property var chainsError: chains => "" + + // 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] - renamed from tokensKey + 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 + 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 + + position [int] - if custom order available, display position fetched from ManageTokensController + 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 + + 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 key: model.tokensKey + + 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.symbol) + + 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 int position: { + controller.revision + return controller.order(model.symbol) + } + + readonly property bool visible: { + root.controller.revision + + if (!root.controller.filterAcceptsSymbol(model.symbol)) + return false + + if (hasCommunityId) + return true + + return balance * marketPrice >= root.marketValueThreshold + } + + readonly property url icon: + !!model.image ? model.image + : Constants.tokenIcon(model.symbol, false) + + readonly property url communityIcon: model.communityImage ?? "" + + readonly property bool canBeHidden: model.symbol !== Constants.ethToken + + SortFilterProxyModel { + id: filteredBalances + + sourceModel: rootModel.balances + + filters: [ + FastExpressionFilter { + expression: root.chains.includes(model.chainId) + expectedRoles: ["chainId"] + }, + FastExpressionFilter { + expression: root.accounts.includes(model.account) + expectedRoles: ["account"] + } + ] + } + + 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] + } + } + + expectedRoles: + ["tokensKey", "symbol", "image", "balances", "decimals", + "detailsLoading", "marketDetails", "communityId", "communityImage"] + exposedRoles: + ["key", "error", "balance", "balanceText", "position", "icon", + "visible", "canBeHidden", "marketDetailsAvailable", "marketDetailsLoading", + "marketPrice", "marketChangePct24hour", "communityIcon"] + } + + SortFilterProxyModel { + id: sfpm + + sourceModel: proxyModel + + filters: ValueFilter { + roleName: "visible" + value: true + } + } +} diff --git a/ui/imports/shared/views/qmldir b/ui/imports/shared/views/qmldir index a8b9bedcd9..59c4be3987 100644 --- a/ui/imports/shared/views/qmldir +++ b/ui/imports/shared/views/qmldir @@ -1,5 +1,6 @@ AssetContextMenu 1.0 AssetContextMenu.qml AssetsView 1.0 AssetsView.qml +AssetsViewAdaptor 1.0 AssetsViewAdaptor.qml AssetsViewNew 1.0 AssetsViewNew.qml ConfirmHideAssetPopup 1.0 ConfirmHideAssetPopup.qml ConfirmHideCommunityAssetsPopup 1.0 ConfirmHideCommunityAssetsPopup.qml