status-desktop/ui/imports/shared/popups/send/panels/HoldingSelector.qml

390 lines
15 KiB
QML

import QtQml 2.15
import QtQuick 2.15
import QtQuick.Layouts 1.15
import SortFilterProxyModel 0.2
import StatusQ 0.1
import StatusQ.Controls 0.1
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Components 0.1
import utils 1.0
import shared.controls 1.0
import shared.popups 1.0
import shared.popups.send 1.0
import "../controls"
Item {
id: root
property var assetsModel
property var collectiblesModel
property var networksModel
property bool onlyAssets: true
property string searchText
implicitWidth: holdingItemSelector.implicitWidth
implicitHeight: holdingItemSelector.implicitHeight
property var formatCurrentCurrencyAmount: function(balance){}
property var formatCurrencyAmountFromBigInt: function(balance, symbol, decimals){}
signal itemHovered(string holdingId, var holdingType)
signal itemSelected(string holdingId, var holdingType)
property alias selectedItem: holdingItemSelector.selectedItem
property alias hoveredItem: holdingItemSelector.hoveredItem
property string searchPlaceholderText: {
if (d.isCurrentBrowsingTypeAsset) {
return qsTr("Search for token or enter token address")
} else if (d.isBrowsingGroup) {
return qsTr("Search %1").arg(d.currentBrowsingGroupName ?? qsTr("collectibles in collection"))
} else {
return qsTr("Search collectibles")
}
}
function setSelectedItem(item, holdingType) {
d.browsingHoldingType = holdingType
holdingItemSelector.selectedItem = null
d.currentHoldingType = holdingType
holdingItemSelector.selectedItem = item
}
function setHoveredItem(item, holdingType) {
d.browsingHoldingType = holdingType
holdingItemSelector.hoveredItem = null
d.currentHoldingType = holdingType
holdingItemSelector.hoveredItem = item
}
QtObject {
id: d
// Internal management properties and signals:
readonly property var holdingTypes: onlyAssets ?
[Constants.TokenType.ERC20] :
[Constants.TokenType.ERC20, Constants.TokenType.ERC721]
readonly property var tabsModel: onlyAssets ?
[qsTr("Assets")] :
[qsTr("Assets"), qsTr("Collectibles")]
readonly property var updateSearchText: Backpressure.debounce(root, 500, function(inputText) {
root.searchText = inputText
})
function isAsset(type) {
return type === Constants.TokenType.ERC20
}
function isCommunityItem(type) {
return type === Constants.CollectiblesNestedItemType.CommunityCollectible ||
type === Constants.CollectiblesNestedItemType.Community
}
function isGroupItem(type) {
return type === Constants.CollectiblesNestedItemType.Collection ||
type === Constants.CollectiblesNestedItemType.Community
}
property int browsingHoldingType: Constants.TokenType.ERC20
readonly property bool isCurrentBrowsingTypeAsset: isAsset(browsingHoldingType)
readonly property bool isBrowsingGroup: !isCurrentBrowsingTypeAsset && !!root.collectiblesModel && root.collectiblesModel.currentGroupId !== ""
property string currentBrowsingGroupName
property var currentHoldingType: Constants.TokenType.Unknown
readonly property string uppercaseSearchText: searchText.toUpperCase()
property var assetTextFn: function (asset) {
return !!asset && asset.symbol ? asset.symbol : ""
}
property var assetIconSourceFn: function (asset) {
if (!asset) {
return ""
} else if (asset.image) {
// Community assets have a dedicated image streamed from status-go
return asset.image
}
return Constants.tokenIcon(asset.symbol)
}
property var collectibleTextFn: function (item) {
if (!!item) {
return !!item.groupName ? item.groupName + ": " + item.name : item.name
}
return ""
}
property var collectibleIconSourceFn: function (item) {
return !!item && item.iconUrl ? item.iconUrl : ""
}
readonly property RolesRenamingModel renamedAllNetworksModel: RolesRenamingModel {
sourceModel: root.networksModel
mapping: RoleRename {
from: "iconUrl"
to: "networkIconUrl"
}
}
readonly property LeftJoinModel collectibleNetworksJointModel: LeftJoinModel {
leftModel: root.collectiblesModel
rightModel: d.renamedAllNetworksModel
joinRole: "chainId"
}
property var collectibleComboBoxModel: SortFilterProxyModel {
sourceModel: d.collectibleNetworksJointModel
proxyRoles: [
FastExpressionRole {
name: "isCommunityAsset"
expression: d.isCommunityItem(model.itemType)
expectedRoles: ["itemType"]
},
FastExpressionRole {
name: "isGroup"
expression: d.isGroupItem(model.itemType)
expectedRoles: ["itemType"]
}
]
filters: [
ExpressionFilter {
expression: {
return d.uppercaseSearchText === "" || name.toUpperCase().startsWith(d.uppercaseSearchText)
}
}
]
sorters: [
RoleSorter {
roleName: "isCommunityAsset"
sortOrder: Qt.DescendingOrder
},
RoleSorter {
roleName: "isGroup"
sortOrder: Qt.DescendingOrder
}
]
}
// By design values:
readonly property int padding: 16
readonly property int headerTopMargin: 5
readonly property int tabBarTopMargin: 20
readonly property int tabBarHeight: 35
readonly property int bottomInset: 20
readonly property int assetContentIconSize: 21
readonly property int collectibleContentIconSize: 28
readonly property int assetContentTextSize: 28
readonly property int collectibleContentTextSize: 15
}
HoldingItemSelector {
id: holdingItemSelector
width: parent.width
height: parent.height
defaultIconSource: Style.png("tokens/DEFAULT-TOKEN@3x")
placeholderText: d.isCurrentBrowsingTypeAsset ? qsTr("Select asset") : qsTr("Select collectible")
property bool hasCommunityTokens: false
comboBoxDelegate: Item {
property var itemModel: model // read 'model' from the delegate's context
width: loader.width
height: loader.height
Loader {
id: loader
// inject model properties to the loaded item's context
// common
property var model: itemModel
property var chainId: model.chainId
property var name: model.name
property var tokenType: model.tokenType
// asset
property var symbol: model.symbol
property var totalBalance: model.totalBalance
property var marketDetails: model.marketDetails
property var decimals: model.decimals
property var balances: model.balances
// collectible
property var uid: model.uid
property var iconUrl: model.iconUrl
property var networkIconUrl: model.networkIconUrl
property var groupId: model.groupId
property var groupName: model.groupName
property var isGroup: model.isGroup
property var count: model.count
sourceComponent: d.isCurrentBrowsingTypeAsset ? assetComboBoxDelegate : collectibleComboBoxDelegate
}
}
comboBoxModel: d.isCurrentBrowsingTypeAsset
? root.assetsModel
: d.collectibleComboBoxModel
comboBoxPopupHeader: headerComponent
itemTextFn: d.isCurrentBrowsingTypeAsset ? d.assetTextFn : d.collectibleTextFn
itemIconSourceFn: d.isCurrentBrowsingTypeAsset ? d.assetIconSourceFn : d.collectibleIconSourceFn
onComboBoxModelChanged: updateHasCommunityTokens()
function updateHasCommunityTokens() {
hasCommunityTokens = Helpers.modelHasCommunityTokens(comboBoxModel, d.isCurrentBrowsingTypeAsset)
}
contentIconSize: d.isAsset(d.currentHoldingType) ? d.assetContentIconSize : d.collectibleContentIconSize
contentTextSize: d.isAsset(d.currentHoldingType) ? d.assetContentTextSize : d.collectibleContentTextSize
comboBoxListViewSection.property: "isCommunityAsset"
// TODO allow for different header/sections for the Swap modal
comboBoxListViewSection.delegate: AssetsSectionDelegate {
height: !!text ? 52 : 0 // if we bind to some property instead of hardcoded value it wont work nice when switching tabs or going inside collection and back
width: ListView.view.width
required property bool section
text: Helpers.assetsSectionTitle(section, holdingItemSelector.hasCommunityTokens, d.isBrowsingGroup, d.isCurrentBrowsingTypeAsset)
onInfoButtonClicked: Global.openPopup(communityInfoPopupCmp)
}
comboBoxControl.popup.onOpened: comboBoxControl.popup.contentItem.headerItem.focusSearch()
comboBoxControl.popup.onClosed: comboBoxControl.popup.contentItem.headerItem.clear()
comboBoxControl.popup.x: root.width - comboBoxControl.popup.width
}
Component {
id: communityInfoPopupCmp
CommunityAssetsInfoPopup {}
}
Component {
id: headerComponent
ColumnLayout {
function focusSearch() {
searchInput.input.forceActiveFocus()
}
function clear() {
searchInput.input.edit.clear()
}
width: holdingItemSelector.comboBoxControl.popup.width
Layout.topMargin: d.headerTopMargin
spacing: -1 // Used to overlap rectangles from row components
StatusTabBar {
id: tabBar
visible: !root.onlyAssets
Layout.preferredHeight: d.tabBarHeight
Layout.fillWidth: true
Layout.leftMargin: d.padding
Layout.rightMargin: d.padding
Layout.topMargin: d.tabBarTopMargin
Layout.bottomMargin: 6
currentIndex: d.holdingTypes.indexOf(d.browsingHoldingType)
onCurrentIndexChanged: {
if (currentIndex >= 0) {
d.browsingHoldingType = d.holdingTypes[currentIndex]
}
}
Repeater {
id: tabLabelsRepeater
model: d.tabsModel
StatusTabButton {
text: modelData
width: implicitWidth
}
}
}
CollectibleBackButtonWithInfo {
Layout.fillWidth: true
visible: d.isBrowsingGroup
count: collectiblesModel ? collectiblesModel.count : 0
name: d.currentBrowsingGroupName
onBackClicked: {
if (!d.isCurrentBrowsingTypeAsset) {
searchInput.reset()
root.collectiblesModel.currentGroupId = ""
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: searchInput.input.implicitHeight
color: "transparent"
border.color: Theme.palette.baseColor2
border.width: 1
StatusInput {
id: searchInput
anchors.fill: parent
input.showBackground: false
placeholderText: root.searchPlaceholderText
onTextChanged: Qt.callLater(d.updateSearchText, text)
input.clearable: true
input.implicitHeight: 56
input.rightComponent: StatusFlatRoundButton {
icon.name: "search"
type: StatusFlatRoundButton.Type.Secondary
enabled: false
}
}
}
}
}
Component {
id: assetComboBoxDelegate
TokenBalancePerChainDelegate {
objectName: "AssetSelector_ItemDelegate_" + symbol
width: holdingItemSelector.comboBoxControl.popup.width
highlighted: !!holdingItemSelector.selectedItem && symbol === holdingItemSelector.selectedItem.symbol
balancesModel: LeftJoinModel {
leftModel: balances
rightModel: root.networksModel
joinRole: "chainId"
}
onTokenSelected: function (selectedToken) {
holdingItemSelector.selectedItem = selectedToken
d.currentHoldingType = Constants.TokenType.ERC20
root.itemSelected(selectedToken.symbol, Constants.TokenType.ERC20)
holdingItemSelector.comboBoxControl.popup.close()
}
formatCurrentCurrencyAmount: function(balance){
return root.formatCurrentCurrencyAmount(balance)
}
formatCurrencyAmountFromBigInt: function(balance, symbol, decimals){
return root.formatCurrencyAmountFromBigInt(balance, symbol, decimals)
}
}
}
Component {
id: collectibleComboBoxDelegate
CollectibleNestedDelegate {
objectName: "CollectibleSelector_ItemDelegate_" + groupId
width: holdingItemSelector.comboBoxControl.popup.width
highlighted: !!holdingItemSelector.selectedItem && uid === holdingItemSelector.selectedItem.uid
onItemSelected: {
if (isGroup) {
d.currentBrowsingGroupName = groupName
root.collectiblesModel.currentGroupId = groupId
} else {
holdingItemSelector.selectedItem = selectedItem
d.currentHoldingType = tokenType
root.itemSelected(selectedItem.uid, tokenType)
holdingItemSelector.comboBoxControl.popup.close()
}
}
}
}
}