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

392 lines
16 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 controller
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) {
// NOTE Backend returns ETH, SNT, STT and DAI by default
if (!root.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)
}
function getTotalBalance(balances, decimals, key) {
let totalBalance = 0
let nwFilters = root.networkFilters.split(":")
let addrFilters = root.addressFilters.split(":")
for(let i=0; i<balances.count; i++) {
let balancePerAddressPerChain = ModelUtils.get(balances, i)
if (nwFilters.includes(balancePerAddressPerChain.chainId+"") &&
addrFilters.includes(balancePerAddressPerChain.account)) {
totalBalance+=SQUtils.AmountsArithmetic.toNumber(balancePerAddressPerChain[key], decimals)
}
}
return totalBalance
}
property SortFilterProxyModel customSFPM: SortFilterProxyModel {
sourceModel: root.controller.sourceModel
proxyRoles: [
FastExpressionRole {
name: "currentBalance"
expression: d.getTotalBalance(model.balances, model.decimals, "balance")
expectedRoles: ["balances", "decimals"]
},
FastExpressionRole {
name: "currentCurrencyBalance"
expression: {
if(!model.communityId) {
if (!!model.marketDetails) {
return model.currentBalance * model.marketDetails.currencyPrice.amount
}
return 0
}
else {
return model.currentBalance
}
}
expectedRoles: ["marketDetails", "communityId", "currentBalance"]
},
FastExpressionRole {
name: "tokenPrice"
expression: model.marketDetails.currencyPrice.amount
expectedRoles: ["marketDetails"]
},
FastExpressionRole {
name: "change1DayFiat"
expression: {
if (!model.isCommunityAsset && !!model.marketDetails) {
const balance1DayAgo = d.getTotalBalance(model.balances, model.decimals, "balance1DayAgo")
const change = (model.currentBalance * model.marketDetails.currencyPrice.amount) - (balance1DayAgo * (model.marketDetails.currencyPrice.amount - model.marketDetails.change24hour))
return change
}
return 0
}
expectedRoles: ["marketDetails", "balances", "decimals", "currentBalance", "isCommunityAsset"]
},
FastExpressionRole {
name: "isCommunityAsset"
expression: !!model.communityId
expectedRoles: ["communityId"]
}
]
filters: [
FastExpressionFilter {
expression: {
root.controller.revision
return d.tokenIsVisible(model.symbol)
}
expectedRoles: ["symbol"]
}
]
sorters: [
RoleSorter {
roleName: "isCommunityAsset"
},
FastExpressionSorter {
expression: {
root.controller.revision
return root.controller.compareTokens(modelLeft.symbol, modelRight.symbol)
}
enabled: d.isCustomView
expectedRoles: ["symbol"]
},
RoleSorter {
roleName: cmbTokenOrder.currentSortRoleName
sortOrder: cmbTokenOrder.currentSortOrder
enabled: !d.isCustomView
}
]
}
}
Settings {
id: settings
category: "AssetsViewSortSettings"
property int currentSortValue: SortOrderComboBox.TokenOrderCurrencyBalance
property alias currentSortOrder: cmbTokenOrder.currentSortOrder
}
Component.onCompleted: {
settings.sync()
cmbTokenOrder.currentIndex = cmbTokenOrder.indexOfValue(settings.currentSortValue)
}
Component.onDestruction: {
settings.currentSortValue = cmbTokenOrder.currentValue
}
ColumnLayout {
Layout.fillWidth: true
Layout.preferredHeight: root.filterVisible ? implicitHeight : 0
spacing: 20
opacity: root.filterVisible ? 1 : 0
visible: opacity > 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: root.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.TokenOrder1DChange, text: qsTr("1d change: balance value"), icon: "history", sortRoleName: "change1DayFiat" }, // custom SFPM ExpressionRole
{ 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
AssetsSectionDelegate {
width: parent.width
text: qsTr("Community minted")
onOpenInfoPopup: 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 currencyStore.formatCurrencyAmount(totalCurrencyBalance, currencyStore.currentCurrency)
}
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
visibleOnDisabled: true
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.openConfirmHideAssetPopup(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
CommunityAssetsInfoPopup {}
}
Component {
id: confirmHideCommunityAssetsPopup
ConfirmationDialog {
property string communityId
property string communityName
property string communityImage
width: 520
destroyOnClose: true
confirmButtonLabel: qsTr("Hide '%1' assets").arg(communityName)
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: {
root.controller.showHideGroup(communityId, false)
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,
""
)
}
}
}
}