status-desktop/ui/imports/shared/views/AssetsView.qml

461 lines
19 KiB
QML

import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import Qt.labs.settings 1.1
import StatusQ 0.1
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Core.Utils 0.1 as SQUtils
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import StatusQ.Popups 0.1
import StatusQ.Popups.Dialog 0.1
import StatusQ.Models 0.1
import StatusQ.Internal 0.1
import SortFilterProxyModel 0.2
import utils 1.0
import shared.stores 1.0
import shared.controls 1.0
import shared.popups 1.0
import AppLayouts.Wallet.controls 1.0
ColumnLayout {
id: root
// expected roles: name, symbol, balances, currencyPrice, changePct24hour, communityId, communityName, communityImage
required property var assets
property var currencyStore
property var networkConnectionStore
property var overview
property bool assetDetailsLaunched: false
property bool filterVisible
property bool areAssetsLoading: false
property string addressFilters
property string networkFilters
signal assetClicked(var token)
signal sendRequested(string symbol)
signal receiveRequested(string symbol)
signal switchToCommunityRequested(string communityId)
signal manageTokensRequested()
spacing: 0
QtObject {
id: d
property int selectedAssetIndex: -1
readonly property int loadingItemsCount: 25
readonly property bool isCustomView: cmbTokenOrder.currentValue === SortOrderComboBox.TokenOrderCustom
function tokenIsVisible(symbol, currencyBalance) {
// NOTE Backend returns ETH, SNT, STT and DAI by default
if (!d.controller.filterAcceptsSymbol(symbol)) // explicitely hidden
return false
// Received tokens can have 0 balance, which indicate previosuly owned token
return true // TODO handle UI threshold (#12611)
}
readonly property var controller: ManageTokensController {
settingsKey: "WalletAssets"
}
function hideAllCommunityTokens(communityId) {
const tokenSymbols = ModelUtils.getAll(assetsListView.model, "symbol", "communityId", communityId)
d.controller.settingsHideCommunityTokens(communityId, tokenSymbols)
}
readonly property SubmodelProxyModel assetsWithFilteredBalances: SubmodelProxyModel {
id: assetsWithFilteredBalances
sourceModel: root.assets
submodelRoleName: "balances"
delegateModel: SortFilterProxyModel {
sourceModel: submodel
filters: FastExpressionFilter {
expression: {
root.networkFilters
root.addressFilters
return root.networkFilters.split(":").includes(model.chainId+"") &&
(!!root.addressFilters ? root.addressFilters.toUpperCase() === model.account.toUpperCase() : true)
}
expectedRoles: ["chainId", "account"]
}
}
}
function getTotalBalance(balances, decimals) {
let totalBalance = 0
for(let i=0; i<balances.count; i++) {
totalBalance+=SQUtils.AmountsArithmetic.toNumber(ModelUtils.get(balances, i, "balance"), decimals)
}
return totalBalance
}
property SortFilterProxyModel customSFPM: SortFilterProxyModel {
sourceModel: d.assetsWithFilteredBalances
proxyRoles: [
FastExpressionRole {
id: filter
name: "currentBalance"
expression: d.getTotalBalance(model.balances, model.decimals, root.addressFilters, root.networkFilters)
expectedRoles: ["balances", "decimals"]
},
FastExpressionRole {
name: "currentCurrencyBalance"
expression: {
if(!model.communityId) {
return model.currentBalance * model.marketDetails.currencyPrice.amount
}
else {
return model.currentBalance
}
}
expectedRoles: ["marketDetails", "communityId", "currentBalance"]
},
FastExpressionRole {
name: "tokenPrice"
expression: model.marketDetails.currencyPrice.amount
expectedRoles: ["marketDetails"]
},
FastExpressionRole {
name: "changePct24hour"
expression: model.marketDetails.changePct24hour
expectedRoles: ["marketDetails"]
},
FastExpressionRole {
name: "isCommunityAsset"
expression: !!model.communityId
expectedRoles: ["communityId"]
}
]
filters: [
FastExpressionFilter {
expression: {
d.controller.settingsDirty
return d.tokenIsVisible(model.symbol, model.currentCurrencyBalance)
}
expectedRoles: ["symbol", "currentCurrencyBalance"]
}
]
sorters: [
RoleSorter {
roleName: "isCommunityAsset"
},
FastExpressionSorter {
expression: {
d.controller.settingsDirty
return d.controller.lessThan(modelLeft.symbol, modelRight.symbol)
}
enabled: d.isCustomView
expectedRoles: ["symbol"]
},
RoleSorter {
roleName: cmbTokenOrder.currentSortRoleName
sortOrder: cmbTokenOrder.currentSortOrder
enabled: !d.isCustomView
}
]
}
}
Settings {
category: "AssetsViewSortSettings"
property alias currentSortField: cmbTokenOrder.currentIndex
property alias currentSortOrder: cmbTokenOrder.currentSortOrder
}
ColumnLayout {
Layout.fillWidth: true
Layout.preferredHeight: root.filterVisible ? implicitHeight : 0
spacing: 20
opacity: root.filterVisible ? 1 : 0
Behavior on Layout.preferredHeight { NumberAnimation { duration: 200; easing.type: Easing.InOutQuad } }
Behavior on opacity { NumberAnimation { duration: 200; easing.type: Easing.InOutQuad } }
StatusDialogDivider {
Layout.fillWidth: true
}
RowLayout {
Layout.fillWidth: true
spacing: Style.current.halfPadding
StatusBaseText {
color: Theme.palette.baseColor1
font.pixelSize: Style.current.additionalTextSize
text: qsTr("Sort by:")
}
SortOrderComboBox {
id: cmbTokenOrder
hasCustomOrderDefined: d.controller.hasSettings
model: [
{ value: SortOrderComboBox.TokenOrderCurrencyBalance, text: qsTr("Asset balance value"), icon: "token-sale", sortRoleName: "currentCurrencyBalance" }, // custom SFPM ExpressionRole on "enabledNetworkCurrencyBalance" amount
{ value: SortOrderComboBox.TokenOrderBalance, text: qsTr("Asset balance"), icon: "channel", sortRoleName: "currentBalance" }, // custom SFPM ExpressionRole on "enabledNetworkBalance" amount
{ value: SortOrderComboBox.TokenOrderCurrencyPrice, text: qsTr("Asset value"), icon: "token", sortRoleName: "tokenPrice" }, // custom SFPM ExpressionRole on "currencyPrice" amount
{ value: SortOrderComboBox.TokenOrder1WChange, text: qsTr("1d change: balance value"), icon: "history", sortRoleName: "changePct24hour" }, // FIXME changePct1week role missing in backend!!!
{ value: SortOrderComboBox.TokenOrderAlpha, text: qsTr("Asset name"), icon: "bold", sortRoleName: "name" },
{ value: SortOrderComboBox.TokenOrderCustom, text: qsTr("Custom order"), icon: "exchange", sortRoleName: "" },
{ value: SortOrderComboBox.TokenOrderNone, text: "---", icon: "", sortRoleName: "" }, // separator
{ value: SortOrderComboBox.TokenOrderCreateCustom, text: hasCustomOrderDefined ? qsTr("Edit custom order →") : qsTr("Create custom order →"),
icon: "", sortRoleName: "" }
]
onCreateOrEditRequested: {
root.manageTokensRequested()
}
}
}
StatusDialogDivider {
Layout.fillWidth: true
}
}
StatusListView {
id: assetsListView
Layout.fillWidth: true
Layout.topMargin: Style.current.padding
Layout.preferredHeight: contentHeight
Layout.fillHeight: true
objectName: "assetViewStatusListView"
model: root.areAssetsLoading ? d.loadingItemsCount : d.customSFPM
delegate: delegateLoader
section {
property: "isCommunityAsset"
delegate: Loader {
width: ListView.view.width
required property string section
sourceComponent: section === "true" ? sectionDelegate : null
}
}
}
Component {
id: sectionDelegate
ColumnLayout {
width: parent.width
spacing: 0
StatusDialogDivider {
Layout.fillWidth: true
Layout.topMargin: Style.current.padding
Layout.bottomMargin: Style.current.halfPadding
}
RowLayout {
Layout.fillWidth: true
Layout.leftMargin: Style.current.padding
Layout.rightMargin: Style.current.smallPadding
Layout.bottomMargin: 4
StatusBaseText {
text: qsTr("Community assets")
color: Theme.palette.baseColor1
}
Item { Layout.fillWidth: true }
StatusFlatButton {
Layout.preferredWidth: 32
Layout.preferredHeight: 32
icon.name: "info"
textColor: Theme.palette.baseColor1
horizontalPadding: 0
verticalPadding: 0
onClicked: Global.openPopup(communityInfoPopupCmp)
}
}
}
}
Component {
id: delegateLoader
Loader {
property var modelData: model
property int delegateIndex: index
width: ListView.view.width
sourceComponent: root.areAssetsLoading ? loadingTokenDelegate : tokenDelegate
}
}
Component {
id: loadingTokenDelegate
LoadingTokenDelegate {
objectName: "AssetView_LoadingTokenDelegate_" + delegateIndex
}
}
Component {
id: tokenDelegate
TokenDelegate {
objectName: "AssetView_TokenListItem_" + (!!modelData ? modelData.symbol : "")
readonly property string balance: !!modelData && !!modelData.currentBalance ? "%1".arg(modelData.currentBalance) : "" // Needed for the tests
errorTooltipText_1: !!modelData && !!networkConnectionStore ? networkConnectionStore.getBlockchainNetworkDownTextForToken(modelData.balances) : ""
errorTooltipText_2: !!networkConnectionStore ? networkConnectionStore.getMarketNetworkDownText() : ""
subTitle: {
if (!modelData || !modelData.symbol) {
return ""
}
if (networkConnectionStore && networkConnectionStore.noTokenBalanceAvailable) {
return ""
}
return LocaleUtils.currencyAmountToLocaleString(root.currencyStore.getCurrencyAmount(modelData.currentBalance, modelData.symbol))
}
currencyBalance.text: {
let totalCurrencyBalance = modelData && modelData.currentCurrencyBalance ? modelData.currentCurrencyBalance : 0
return LocaleUtils.currencyAmountToLocaleString(root.currencyStore.getCurrentCurrencyAmount(totalCurrencyBalance))
}
errorMode: !!networkConnectionStore ? networkConnectionStore.noBlockchainConnectionAndNoCache && !networkConnectionStore.noMarketConnectionAndNoCache : false
errorIcon.tooltip.text: !!networkConnectionStore ? networkConnectionStore.noBlockchainConnectionAndNoCacheText : ""
onClicked: (itemId, mouse) => {
if (mouse.button === Qt.LeftButton) {
RootStore.getHistoricalDataForToken(modelData.symbol, root.currencyStore.currentCurrency)
d.selectedAssetIndex = delegateIndex
assetClicked(assetsListView.model.get(delegateIndex))
} else if (mouse.button === Qt.RightButton) {
Global.openMenu(tokenContextMenu, this,
{symbol: modelData.symbol, assetName: modelData.name, assetImage: symbolUrl,
communityId: modelData.communityId, communityName: modelData.communityName, communityImage: modelData.communityImage})
}
}
onSwitchToCommunityRequested: root.switchToCommunityRequested(communityId)
Component.onCompleted: {
// on Model reset if the detail view is shown, update the data in background.
if(root.assetDetailsLaunched && delegateIndex === d.selectedAssetIndex) {
assetClicked(assetsListView.model.get(delegateIndex))
}
}
}
}
Component {
id: tokenContextMenu
StatusMenu {
onClosed: destroy()
property string symbol
property string assetName
property string assetImage
property string communityId
property string communityName
property string communityImage
StatusAction {
enabled: root.networkConnectionStore.sendBuyBridgeEnabled && !root.overview.isWatchOnlyAccount && root.overview.canSend
icon.name: "send"
text: qsTr("Send")
onTriggered: root.sendRequested(symbol)
}
StatusAction {
icon.name: "receive"
text: qsTr("Receive")
onTriggered: root.receiveRequested(symbol)
}
StatusMenuSeparator {}
StatusAction {
icon.name: "settings"
text: qsTr("Manage tokens")
onTriggered: root.manageTokensRequested()
}
StatusAction {
enabled: symbol !== "ETH"
type: StatusAction.Type.Danger
icon.name: "hide"
text: qsTr("Hide asset")
onTriggered: Global.openPopup(confirmHideAssetPopup, {symbol, assetName, assetImage, communityId})
}
StatusAction {
enabled: !!communityId
type: StatusAction.Type.Danger
icon.name: "hide"
text: qsTr("Hide all assets from this community")
onTriggered: Global.openPopup(confirmHideCommunityAssetsPopup, {communityId, communityName, communityImage})
}
}
}
Component {
id: communityInfoPopupCmp
StatusDialog {
destroyOnClose: true
title: qsTr("What are community assets?")
standardButtons: Dialog.Ok
width: 520
contentItem: StatusBaseText {
wrapMode: Text.Wrap
text: qsTr("Community assets are assets that have been minted by a community. As these assets cannot be verified, always double check their origin and validity before interacting with them. If in doubt, ask a trusted member or admin of the relevant community.")
}
}
}
Component {
id: confirmHideAssetPopup
ConfirmationDialog {
property string symbol
property string assetName
property string assetImage
property string communityId
readonly property string formattedName: assetName + (communityId ? " (" + qsTr("community asset") + ")" : "")
width: 520
destroyOnClose: true
confirmButtonLabel: qsTr("Hide %1").arg(assetName)
cancelBtnType: ""
showCancelButton: true
headerSettings.title: qsTr("Hide %1").arg(formattedName)
headerSettings.asset.name: assetImage
confirmationText: qsTr("Are you sure you want to hide %1? You will no longer see or be able to interact with this asset anywhere inside Status.").arg(formattedName)
onCancelButtonClicked: close()
onConfirmButtonClicked: {
d.controller.settingsHideToken(symbol)
close()
Global.displayToastMessage(
qsTr("%1 was successfully hidden. You can toggle asset visibility via %2.").arg(formattedName)
.arg(`<a style="text-decoration:none" href="#${Constants.appSection.profile}/${Constants.settingsSubsection.wallet}/${Constants.walletSettingsSubsection.manageAssets}">` + qsTr("Settings", "Go to Settings") + "</a>"),
"",
"checkmark-circle",
false,
Constants.ephemeralNotificationType.success,
""
)
}
}
}
Component {
id: confirmHideCommunityAssetsPopup
ConfirmationDialog {
property string communityId
property string communityName
property string communityImage
width: 520
destroyOnClose: true
confirmButtonLabel: qsTr("Hide all assets minted by this community")
cancelBtnType: ""
showCancelButton: true
headerSettings.title: qsTr("Hide %1 community assets").arg(communityName)
headerSettings.asset.name: communityImage
confirmationText: qsTr("Are you sure you want to hide all community assets minted by %1? You will no longer see or be able to interact with these assets anywhere inside Status.").arg(communityName)
onCancelButtonClicked: close()
onConfirmButtonClicked: {
d.hideAllCommunityTokens(communityId)
close()
Global.displayToastMessage(
qsTr("%1 community assets were successfully hidden. You can toggle asset visibility via %2.").arg(communityName)
.arg(`<a style="text-decoration:none" href="#${Constants.appSection.profile}/${Constants.settingsSubsection.wallet}/${Constants.walletSettingsSubsection.manageAssets}">` + qsTr("Settings", "Go to Settings") + "</a>"),
"",
"checkmark-circle",
false,
Constants.ephemeralNotificationType.success,
""
)
}
}
}
}