diff --git a/src/app/modules/shared_models/collectibles_entry.nim b/src/app/modules/shared_models/collectibles_entry.nim index a6cfc1e292..1beffd2101 100644 --- a/src/app/modules/shared_models/collectibles_entry.nim +++ b/src/app/modules/shared_models/collectibles_entry.nim @@ -74,10 +74,10 @@ QtObject: proc getCollectionData(self: CollectiblesEntry): backend.CollectionData = return self.data.collectionData.get() - proc hasCommunityData(self: CollectiblesEntry): bool = + proc hasCommunityData*(self: CollectiblesEntry): bool = return self.data != nil and isSome(self.data.communityData) - proc getCommunityData(self: CollectiblesEntry): backend.CommunityData = + proc getCommunityData*(self: CollectiblesEntry): backend.CommunityData = return self.data.communityData.get() proc hasOwnership(self: CollectiblesEntry): bool = @@ -364,4 +364,4 @@ QtObject: tokenID: stint.u256(0) ) let extradata = ExtraData() - return newCollectibleDetailsBasicEntry(id, extradata) \ No newline at end of file + return newCollectibleDetailsBasicEntry(id, extradata) diff --git a/src/app/modules/shared_models/collectibles_nested_item.nim b/src/app/modules/shared_models/collectibles_nested_item.nim index a89a7186b1..73e1632ef9 100644 --- a/src/app/modules/shared_models/collectibles_nested_item.nim +++ b/src/app/modules/shared_models/collectibles_nested_item.nim @@ -9,6 +9,7 @@ type collectionId: string collectionName: string isCollection: bool + communityId: string proc initItem*( id: string, @@ -17,7 +18,8 @@ proc initItem*( iconUrl: string, collectionId: string, collectionName: string, - isCollection: bool + isCollection: bool, + communityId: string, ): Item = result.id = id result.chainId = chainId @@ -26,6 +28,7 @@ proc initItem*( result.collectionId = collectionId result.collectionName = collectionName result.isCollection = isCollection + result.communityId = communityId proc `$`*(self: Item): string = result = fmt"""CollectiblesNestedEntry( @@ -36,6 +39,7 @@ proc `$`*(self: Item): string = collectionId: {self.collectionId}, collectionName: {self.collectionName}, isCollection: {self.isCollection}, + communityId: {self.communityId}, ]""" proc getId*(self: Item): string = @@ -58,3 +62,6 @@ proc getCollectionName*(self: Item): string = proc getIsCollection*(self: Item): bool = return self.isCollection + +proc getCommunityId*(self: Item): string = + return self.communityId diff --git a/src/app/modules/shared_models/collectibles_nested_model.nim b/src/app/modules/shared_models/collectibles_nested_model.nim index 23519b9e68..59c396a8d0 100644 --- a/src/app/modules/shared_models/collectibles_nested_model.nim +++ b/src/app/modules/shared_models/collectibles_nested_model.nim @@ -15,6 +15,7 @@ type CollectionUid CollectionName IsCollection + CommunityId QtObject: type @@ -80,6 +81,7 @@ QtObject: CollectiblesNestedRole.CollectionUid.int:"collectionUid", CollectiblesNestedRole.CollectionName.int:"collectionName", CollectiblesNestedRole.IsCollection.int:"isCollection", + CollectiblesNestedRole.CommunityId.int:"communityId", }.toTable method data(self: Model, index: QModelIndex, role: int): QVariant = @@ -107,6 +109,8 @@ QtObject: result = newQVariant(item.getCollectionName()) of CollectiblesNestedRole.IsCollection: result = newQVariant(item.getIsCollection()) + of CollectiblesNestedRole.CommunityId: + result = newQVariant(item.getCommunityId()) proc rowData(self: Model, index: int, column: string): string {.slot.} = if (index >= self.items.len): @@ -120,6 +124,7 @@ QtObject: of "collectionUid": result = item.getCollectionId() of "collectionName": result = item.getCollectionName() of "isCollection": result = $item.getIsCollection() + of "communityId": result = item.getCommunityId() proc getCollectiblesPerCollectionId(items: seq[flat_item.CollectiblesEntry]): Table[string, seq[flat_item.CollectiblesEntry]] = var collectiblesPerCollection = initTable[string, seq[flat_item.CollectiblesEntry]]() @@ -140,10 +145,16 @@ QtObject: for collectionId, collectionCollectibles in collectiblesPerCollection.pairs: if self.currentCollectionUid == "": # No collection selected - # If the collection contains more than 1 collectible, we add a single collection item + # If the collection contains more than 1 collectible or if collectibles has community data, we add a single collection item # Otherwise, we add the collectible - if collectionCollectibles.len > 1: - let collectionItem = collectibleToCollectionNestedItem(collectionCollectibles[0]) + var hasCommunityData: bool + var collectionItem: nested_item.Item + if collectionCollectibles.len > 0: + let firstItem = collectionCollectibles[0] + hasCommunityData = firstItem.hasCommunityData() + collectionItem = collectibleToCollectionNestedItem(firstItem) + + if collectionCollectibles.len > 1 or hasCommunityData: self.items.add(collectionItem) else: for collectible in collectionCollectibles: diff --git a/src/app/modules/shared_models/collectibles_nested_utils.nim b/src/app/modules/shared_models/collectibles_nested_utils.nim index e8200fa459..4fcf0beb86 100644 --- a/src/app/modules/shared_models/collectibles_nested_utils.nim +++ b/src/app/modules/shared_models/collectibles_nested_utils.nim @@ -9,7 +9,8 @@ proc collectibleToCollectibleNestedItem*(flatItem: flat_item.CollectiblesEntry): flatItem.getImageURL(), flatItem.getCollectionID(), flatItem.getCollectionName(), - false + false, + flatItem.getCommunityID() ) proc collectibleToCollectionNestedItem*(flatItem: flat_item.CollectiblesEntry): nested_item.Item = @@ -20,5 +21,6 @@ proc collectibleToCollectionNestedItem*(flatItem: flat_item.CollectiblesEntry): flatItem.getCollectionImageURL(), flatItem.getCollectionID(), flatItem.getCollectionName(), - true + true, + flatItem.getCommunityID() ) diff --git a/storybook/src/Models/WalletNestedCollectiblesModel.qml b/storybook/src/Models/WalletNestedCollectiblesModel.qml index 7243b56c42..8ccc0256dc 100644 --- a/storybook/src/Models/WalletNestedCollectiblesModel.qml +++ b/storybook/src/Models/WalletNestedCollectiblesModel.qml @@ -37,7 +37,16 @@ ListModel { collectionUid: "custom", collectionName: "Custom", isCollection: false, - } + }, + { + uid: "ID-Community1", + chainId: 1, + name: "Community Admin Token", + iconUrl: ModelsData.collectibles.mana, + collectionUid: "community-uid-1", + isCollection: false, + communityId: "community-id-1" + }, ] readonly property var criptoKittiesData: [ diff --git a/ui/imports/shared/controls/AssetsSectionDelegate.qml b/ui/imports/shared/controls/AssetsSectionDelegate.qml index 0c36aa5556..468a0ce46e 100644 --- a/ui/imports/shared/controls/AssetsSectionDelegate.qml +++ b/ui/imports/shared/controls/AssetsSectionDelegate.qml @@ -9,13 +9,15 @@ import StatusQ.Popups.Dialog 0.1 import utils 1.0 ColumnLayout { + id: root + signal openInfoPopup() + property alias text: sectionTitle.text spacing: 0 StatusDialogDivider { Layout.fillWidth: true - Layout.topMargin: Style.current.padding Layout.bottomMargin: Style.current.halfPadding } RowLayout { @@ -24,13 +26,15 @@ ColumnLayout { Layout.rightMargin: Style.current.smallPadding Layout.bottomMargin: 4 StatusBaseText { - text: qsTr("Community assets") + id: sectionTitle + color: Theme.palette.baseColor1 } Item { Layout.fillWidth: true } StatusFlatButton { Layout.preferredWidth: 32 Layout.preferredHeight: 32 + visible: !!root.text icon.name: "info" textColor: Theme.palette.baseColor1 horizontalPadding: 0 @@ -39,4 +43,3 @@ ColumnLayout { } } } - diff --git a/ui/imports/shared/popups/send/Helpers.qml b/ui/imports/shared/popups/send/Helpers.qml index 8ac93ba0d6..b0b808fc1e 100644 --- a/ui/imports/shared/popups/send/Helpers.qml +++ b/ui/imports/shared/popups/send/Helpers.qml @@ -67,4 +67,24 @@ QtObject { return req } + + function assetsSectionTitle(sectionNeeded, hasCommunityTokens, isInsideCollection, isERC20List) { + // if we have non-empty section name, setting section's "visible" property to false does not work properly. + // So we have to return empty section here if we want to hide it + let title = "" + if (!isInsideCollection) { + if (sectionNeeded) { + title = qsTr("Community minted") + } + else { + if (!isERC20List) { + // Show "Other" only if there are "Community minted" tokens on the list + if (hasCommunityTokens) { + title = qsTr("Other") + } + } + } + } + return title + } } diff --git a/ui/imports/shared/popups/send/controls/SearchBoxWithRightIcon.qml b/ui/imports/shared/popups/send/controls/SearchBoxWithRightIcon.qml index 2ec75df09f..9741e98eb2 100644 --- a/ui/imports/shared/popups/send/controls/SearchBoxWithRightIcon.qml +++ b/ui/imports/shared/popups/send/controls/SearchBoxWithRightIcon.qml @@ -5,6 +5,7 @@ import StatusQ.Core.Theme 0.1 StatusInput { property bool showTopBorder: false + property bool showBottomBorder: true placeholderText: qsTr("Search") input.implicitHeight: 56 @@ -23,6 +24,7 @@ StatusInput { color: Theme.palette.baseColor2 } Rectangle { + visible: showBottomBorder anchors.bottom: parent.bottom height: 1 width: parent.width diff --git a/ui/imports/shared/popups/send/panels/HoldingSelector.qml b/ui/imports/shared/popups/send/panels/HoldingSelector.qml index d3483e6747..e3237fb47e 100644 --- a/ui/imports/shared/popups/send/panels/HoldingSelector.qml +++ b/ui/imports/shared/popups/send/panels/HoldingSelector.qml @@ -5,6 +5,7 @@ import QtQuick.Layouts 1.13 import shared.controls 1.0 import shared.popups 1.0 +import shared.popups.send 1.0 import utils 1.0 import SortFilterProxyModel 0.2 @@ -67,7 +68,7 @@ Item { readonly property var updateSearchText: Backpressure.debounce(root, 1000, function(inputText) { searchText = inputText }) - + function isAsset(type) { return type === Constants.TokenType.ERC20 } @@ -116,6 +117,13 @@ Item { property var collectibleComboBoxModel: SortFilterProxyModel { sourceModel: d.collectibleNetworksJointModel + proxyRoles: [ + FastExpressionRole { + name: "isCommunityAsset" + expression: !!model.communityId + expectedRoles: ["communityId"] + } + ] filters: [ ExpressionFilter { expression: { @@ -123,10 +131,16 @@ Item { } } ] - sorters: RoleSorter { - roleName: "isCollection" - sortOrder: Qt.DescendingOrder - } + sorters: [ + RoleSorter { + roleName: "isCommunityAsset" + sortOrder: Qt.DescendingOrder + }, + RoleSorter { + roleName: "isCollection" + sortOrder: Qt.DescendingOrder + } + ] } readonly property string searchPlaceholderText: { @@ -149,6 +163,7 @@ Item { readonly property int collectibleContentIconSize: 28 readonly property int assetContentTextSize: 28 readonly property int collectibleContentTextSize: 15 + } HoldingItemSelector { @@ -158,6 +173,8 @@ Item { defaultIconSource: Style.png("tokens/DEFAULT-TOKEN@3x") placeholderText: d.isCurrentBrowsingTypeAsset ? qsTr("Select token") : qsTr("Select collectible") + property bool hasCommunityTokens: false + comboBoxDelegate: Item { property var itemModel: model // read 'model' from the delegate's context width: loader.width @@ -192,26 +209,28 @@ Item { itemTextFn: d.isCurrentBrowsingTypeAsset ? d.assetTextFn : d.collectibleTextFn itemIconSourceFn: d.isCurrentBrowsingTypeAsset ? d.assetIconSourceFn : d.collectibleIconSourceFn comboBoxModel: d.isCurrentBrowsingTypeAsset ? root.assetsModel : d.collectibleComboBoxModel + onComboBoxModelChanged: updateHasCommunityTokens() + + function updateHasCommunityTokens() { + if (comboBoxModel.count > 0) { + const item = comboBoxModel.get(0) + holdingItemSelector.hasCommunityTokens = item.isCommunityAsset + } + } contentIconSize: d.isAsset(d.currentHoldingType) ? d.assetContentIconSize : d.collectibleContentIconSize contentTextSize: d.isAsset(d.currentHoldingType) ? d.assetContentTextSize : d.collectibleContentTextSize comboBoxListViewSection.property: "isCommunityAsset" - comboBoxListViewSection.delegate: Loader { - width: ListView.view.width - required property string section - sourceComponent: d.isCurrentBrowsingTypeAsset && section === "true" ? sectionDelegate : null - } + comboBoxListViewSection.delegate: AssetsSectionDelegate { + height: !!text ? implicitHeight : 0 // binding loop here. Without implicitHeight, binding on height does not work properly - height is 0 in some cases + width: ListView.view.width + required property bool section + text: Helpers.assetsSectionTitle(section, holdingItemSelector.hasCommunityTokens, d.isBrowsingCollection, d.isCurrentBrowsingTypeAsset) + onOpenInfoPopup: Global.openPopup(communityInfoPopupCmp) + } comboBoxControl.popup.onClosed: comboBoxControl.popup.contentItem.headerItem.clear() } - Component { - id: sectionDelegate - AssetsSectionDelegate { - width: parent.width - onOpenInfoPopup: Global.openPopup(communityInfoPopupCmp) - } - } - Component { id: communityInfoPopupCmp CommunityAssetsInfoPopup {} diff --git a/ui/imports/shared/popups/send/views/TokenListView.qml b/ui/imports/shared/popups/send/views/TokenListView.qml index cf5f2bc455..b44f0f9058 100644 --- a/ui/imports/shared/popups/send/views/TokenListView.qml +++ b/ui/imports/shared/popups/send/views/TokenListView.qml @@ -1,5 +1,6 @@ -import QtQuick 2.13 -import QtQuick.Layouts 1.14 +import QtQuick 2.15 +import QtQml 2.15 +import QtQuick.Layouts 1.15 import SortFilterProxyModel 0.2 @@ -13,6 +14,7 @@ import utils 1.0 import shared.controls 1.0 import shared.popups 1.0 +import shared.popups.send 1.0 import "../controls" Item { @@ -77,6 +79,7 @@ Item { } readonly property bool isBrowsingTypeERC20: root.browsingHoldingType === Constants.TokenType.ERC20 + readonly property bool isBrowsingCollection: !isBrowsingTypeERC20 && !!root.collectibles && root.collectibles.currentCollectionUid !== "" } StatusBaseText { @@ -132,15 +135,73 @@ Item { Layout.fillWidth: true Layout.fillHeight: true - header: d.isBrowsingTypeERC20 ? tokenHeader : collectibleHeader - model: d.isBrowsingTypeERC20 ? root.assets : collectiblesModel - delegate: d.isBrowsingTypeERC20 ? tokenDelegate : collectiblesDelegate + visible: d.isBrowsingTypeERC20 + header: tokenHeader + model: root.assets + delegate: tokenDelegate + + property bool hasCommunityTokens: false + function updateHasCommunityTokens() { + if (count > 0) { + // For assets community minted tokens are at the end of the list by design. + // It is faster than iterate over all the items and check their isCommunityAsset role + const isCommunityAsset = model.get(count - 1).isCommunityAsset + hasCommunityTokens = hasCommunityTokens || isCommunityAsset + } + } + + onCountChanged: updateHasCommunityTokens() section { property: "isCommunityAsset" delegate: Loader { - width: ListView.view.width - required property string section - sourceComponent: d.isBrowsingTypeERC20 && section === "true" ? sectionDelegate : null + required property bool section + width: parent.width + property string sectionTitle: Helpers.assetsSectionTitle(section, tokenList.hasCommunityTokens, d.isBrowsingCollection, d.isBrowsingTypeERC20) + active: !!sectionTitle + sourceComponent: AssetsSectionDelegate { + text: parent.sectionTitle + onOpenInfoPopup: Global.openPopup(communityInfoPopupCmp) + } + } + } + } + + // We have a separate listview for collectibles because if we switch models within one listview, + // sections are not displayed correctly, if we play with their "visible" or "height" properties + // when we want to hide some sections. It feels like sections are not designed to be hidden and + // do not hide well in complex scenarios. + StatusListView { + id: collectiblesList + + Layout.fillWidth: true + Layout.fillHeight: true + + visible: !d.isBrowsingTypeERC20 + header: collectibleHeader + model: collectiblesModel + delegate: collectiblesDelegate + + property bool hasCommunityTokens: false + onCountChanged: updateHasCommunityTokens() + + function updateHasCommunityTokens() { + if (count > 0) { + const isCommunityAsset = model.get(0).isCommunityAsset + hasCommunityTokens = hasCommunityTokens || isCommunityAsset + } + } + + section { + property: "isCommunityAsset" + delegate: Loader { + required property bool section + width: parent.width + property string sectionTitle: Helpers.assetsSectionTitle(section, collectiblesList.hasCommunityTokens, d.isBrowsingCollection, d.isBrowsingTypeERC20) + active: !!sectionTitle + sourceComponent: AssetsSectionDelegate { + text: parent.sectionTitle + onOpenInfoPopup: Global.openPopup(communityInfoPopupCmp) + } } } } @@ -149,6 +210,13 @@ Item { property var collectiblesModel: SortFilterProxyModel { sourceModel: d.collectiblesNetworksJointModel + proxyRoles: [ + FastExpressionRole { + name: "isCommunityAsset" + expression: !!model.communityId + expectedRoles: ["communityId"] + } + ] filters: [ ExpressionFilter { expression: { @@ -156,10 +224,16 @@ Item { } } ] - sorters: RoleSorter { - roleName: "isCollection" - sortOrder: Qt.DescendingOrder - } + sorters: [ + RoleSorter { + roleName: "isCommunityAsset" + sortOrder: Qt.DescendingOrder + }, + RoleSorter { + roleName: "isCollection" + sortOrder: Qt.DescendingOrder + } + ] } Component { @@ -168,7 +242,7 @@ Item { width: ListView.view.width selectedSenderAccount: root.selectedSenderAccount balancesModel: LeftJoinModel { - leftModel: !!model & !!model.balances ? model.balances : nil + leftModel: !!model & !!model.balances ? model.balances : null rightModel: root.networksModel joinRole: "chainId" } @@ -186,6 +260,7 @@ Item { id: tokenHeader SearchBoxWithRightIcon { showTopBorder: !root.onlyAssets + showBottomBorder: false width: ListView.view.width placeholderText: qsTr("Search for token or enter token address") onTextChanged: Qt.callLater(d.updateAssetSearchText, text) @@ -213,7 +288,7 @@ Item { spacing: 0 CollectibleBackButtonWithInfo { Layout.fillWidth: true - visible: !!root.collectibles && root.collectibles.currentCollectionUid !== "" + visible: d.isBrowsingCollection count: root.collectibles.count name: d.currentBrowsingCollectionName onBackClicked: root.collectibles.currentCollectionUid = "" @@ -221,20 +296,13 @@ Item { SearchBoxWithRightIcon { Layout.fillWidth: true showTopBorder: true + showBottomBorder: false placeholderText: qsTr("Search collectibles") onTextChanged: Qt.callLater(d.updateCollectibleSearchText, text) } } } - Component { - id: sectionDelegate - AssetsSectionDelegate { - width: parent.width - onOpenInfoPopup: Global.openPopup(communityInfoPopupCmp) - } - } - Component { id: communityInfoPopupCmp CommunityAssetsInfoPopup {} diff --git a/vendor/status-go b/vendor/status-go index cc708ce0ce..12d70e0ce4 160000 --- a/vendor/status-go +++ b/vendor/status-go @@ -1 +1 @@ -Subproject commit cc708ce0ce83ef6d04f803b2562674dc6bf3b5b1 +Subproject commit 12d70e0ce45a7dcac78c7e323ab81d8a56d996ce