diff --git a/src/app/modules/shared_models/collectibles_entry.nim b/src/app/modules/shared_models/collectibles_entry.nim index c1cd8829e8..975645bd9d 100644 --- a/src/app/modules/shared_models/collectibles_entry.nim +++ b/src/app/modules/shared_models/collectibles_entry.nim @@ -71,10 +71,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 = @@ -361,4 +361,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 e055dfc2b8..755698d537 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,43 +124,91 @@ 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]]() for item in items: let collectionId = item.getCollectionIDAsString() - if not collectiblesPerCollection.hasKey(collectionId): - collectiblesPerCollection[collectionId] = @[] - collectiblesPerCollection[collectionId].add(item) + let communityId = item.getCommunityId() + if communityId == "": + if not collectiblesPerCollection.hasKey(collectionId): + collectiblesPerCollection[collectionId] = @[] + collectiblesPerCollection[collectionId].add(item) return collectiblesPerCollection + proc getCollectiblesPerCommunityId(items: seq[flat_item.CollectiblesEntry]): Table[string, seq[flat_item.CollectiblesEntry]] = + var collectiblesPerCommunity = initTable[string, seq[flat_item.CollectiblesEntry]]() + + for item in items: + let communityId = item.getCommunityId() + if communityId != "": + if not collectiblesPerCommunity.hasKey(communityId): + collectiblesPerCommunity[communityId] = @[] + collectiblesPerCommunity[communityId].add(item) + + return collectiblesPerCommunity + + proc getNumberOfCollectiblesInCommunity*(self: Model, commId: string): int {.slot.} = + if commId != "": + var collectiblesPerCommunity = getCollectiblesPerCommunityId(self.flatModel.getItems()) + if collectiblesPerCommunity.hasKey(commId): + result = collectiblesPerCommunity[commId].len + + proc getNumberOfCollectiblesInCollection*(self: Model, collUid: string): int {.slot.} = + if collUid != "": + var collectiblesPerCollection = getCollectiblesPerCollectionId(self.flatModel.getItems()) + if collectiblesPerCollection.hasKey(collUid): + result = collectiblesPerCollection[collUid].len + proc refreshItems*(self: Model) {.slot.} = self.beginResetModel() self.items = @[] - var collectiblesPerCollection = getCollectiblesPerCollectionId(self.flatModel.getItems()) - for collectionId, collectionCollectibles in collectiblesPerCollection.pairs: + var addCollections = true + # Add communities + var collectiblesPerCommunity = getCollectiblesPerCommunityId(self.flatModel.getItems()) + for communityId, communityCollectibles in collectiblesPerCommunity.pairs: if self.currentCollectionUid == "": # No collection selected - # If the collection contains more than 1 collectible, we add a single collection item - # Otherwise, we add the collectible - if collectionCollectibles.len > 1: - let collectionItem = collectibleToCollectionNestedItem(collectionCollectibles[0]) - self.items.add(collectionItem) - else: - for collectible in collectionCollectibles: - let collectibleItem = collectibleToCollectibleNestedItem(collectible) - self.items.add(collectibleItem) + if communityCollectibles.len > 0: + let communityItem = collectibleToCollectionNestedItem(communityCollectibles[0]) + self.items.add(communityItem) else: - if self.currentCollectionUid == collectionId: - for collectible in collectionCollectibles: + if self.currentCollectionUid == communityId: + for collectible in communityCollectibles: let collectibleItem = collectibleToCollectibleNestedItem(collectible) self.items.add(collectibleItem) - # No need to keep looking + + # Inside community folder we dont add collection items + addCollections = false break + if addCollections: + # Add collections and collection items + var collectiblesPerCollection = getCollectiblesPerCollectionId(self.flatModel.getItems()) + 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 + # Otherwise, we add the collectible + if collectionCollectibles.len > 1: + let collectionItem = collectibleToCollectionNestedItem(collectionCollectibles[0]) + self.items.add(collectionItem) + else: + for collectible in collectionCollectibles: + let collectibleItem = collectibleToCollectibleNestedItem(collectible) + self.items.add(collectibleItem) + else: + if self.currentCollectionUid == collectionId: + for collectible in collectionCollectibles: + let collectibleItem = collectibleToCollectibleNestedItem(collectible) + self.items.add(collectibleItem) + # No need to keep looking + break + self.endResetModel() self.countChanged() diff --git a/src/app/modules/shared_models/collectibles_nested_utils.nim b/src/app/modules/shared_models/collectibles_nested_utils.nim index d2a6359e0a..865d3c4338 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.getCollectionIDAsString(), 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.getCollectionIDAsString(), 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..4c8df17bd8 100644 --- a/ui/imports/shared/popups/send/Helpers.qml +++ b/ui/imports/shared/popups/send/Helpers.qml @@ -67,4 +67,35 @@ QtObject { return req } + + function assetsSectionTitle(sectionNeeded, hasCommunityTokens, isInsideCollection, isERC20List) { + 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 + } + + function modelHasCommunityTokens(model, isERC20List) { + if (model.count > 0) { + let item + if (isERC20List) { + item = model.get(model.count - 1) + } else { + item = model.get(0) + } + return item.isCommunityAsset + } + + return false + } } diff --git a/ui/imports/shared/popups/send/controls/CollectibleNestedDelegate.qml b/ui/imports/shared/popups/send/controls/CollectibleNestedDelegate.qml index 7425554836..d7a872bdb9 100644 --- a/ui/imports/shared/popups/send/controls/CollectibleNestedDelegate.qml +++ b/ui/imports/shared/popups/send/controls/CollectibleNestedDelegate.qml @@ -41,6 +41,8 @@ StatusListItem { onClicked: d.selectItem() + property int numItems + components: [ StatusRoundedImage { width: 20 @@ -48,6 +50,13 @@ StatusListItem { image.source: Style.svg("tiny/%1".arg(networkIconUrl)) visible: !isCollection && root.sensor.containsMouse }, + StatusBaseText { + id: label + text: root.numItems + font.pixelSize: 13 + color: Theme.palette.baseColor1 + visible: isCollection + }, StatusIcon { icon: "tiny/chevron-right" color: Theme.palette.baseColor1 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..6990cb3765 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 @@ -181,6 +198,7 @@ Item { property var iconUrl: model.iconUrl property var networkIconUrl: model.networkIconUrl property var collectionUid: model.collectionUid + property var communityId: model.communityId property var collectionName: model.collectionName property var isCollection: model.isCollection @@ -192,26 +210,25 @@ 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() { + 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" - comboBoxListViewSection.delegate: Loader { - width: ListView.view.width - required property string section - sourceComponent: d.isCurrentBrowsingTypeAsset && section === "true" ? sectionDelegate : null - } + 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.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 {} @@ -263,6 +280,7 @@ Item { name: d.currentBrowsingCollectionName onBackClicked: { if (!d.isCurrentBrowsingTypeAsset) { + searchInput.reset() root.collectiblesModel.currentCollectionUid = "" } } @@ -325,10 +343,16 @@ Item { CollectibleNestedDelegate { objectName: "CollectibleSelector_ItemDelegate_" + collectionUid width: holdingItemSelector.comboBoxControl.popup.width + numItems: isCollection ? (!!communityId ? + root.collectiblesModel.getNumberOfCollectiblesInCommunity(communityId) : + root.collectiblesModel.getNumberOfCollectiblesInCollection(collectionUid)) : 0 onItemSelected: { if (isCollection) { d.currentBrowsingCollectionName = collectionName - root.collectiblesModel.currentCollectionUid = collectionUid + if (!!communityId) + root.collectiblesModel.currentCollectionUid = communityId + else + root.collectiblesModel.currentCollectionUid = collectionUid } else { holdingItemSelector.selectedItem = selectedItem d.currentHoldingType = Constants.TokenType.ERC721 diff --git a/ui/imports/shared/popups/send/views/TokenListView.qml b/ui/imports/shared/popups/send/views/TokenListView.qml index cf5f2bc455..4b59cc27e7 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 { @@ -135,12 +138,21 @@ Item { header: d.isBrowsingTypeERC20 ? tokenHeader : collectibleHeader model: d.isBrowsingTypeERC20 ? root.assets : collectiblesModel delegate: d.isBrowsingTypeERC20 ? tokenDelegate : collectiblesDelegate + + property bool hasCommunityTokens: false + function updateHasCommunityTokens() { + hasCommunityTokens = Helpers.modelHasCommunityTokens(model, d.isBrowsingTypeERC20) + } + + onModelChanged: updateHasCommunityTokens() section { property: "isCommunityAsset" - delegate: Loader { - width: ListView.view.width - required property string section - sourceComponent: d.isBrowsingTypeERC20 && section === "true" ? sectionDelegate : null + delegate: AssetsSectionDelegate { + required property bool section + width: parent.width + 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 + text: Helpers.assetsSectionTitle(section, tokenList.hasCommunityTokens, d.isBrowsingCollection, d.isBrowsingTypeERC20) + onOpenInfoPopup: Global.openPopup(communityInfoPopupCmp) } } } @@ -149,6 +161,13 @@ Item { property var collectiblesModel: SortFilterProxyModel { sourceModel: d.collectiblesNetworksJointModel + proxyRoles: [ + FastExpressionRole { + name: "isCommunityAsset" + expression: !!model.communityId + expectedRoles: ["communityId"] + } + ] filters: [ ExpressionFilter { expression: { @@ -156,10 +175,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 +193,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 +211,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) @@ -195,11 +221,17 @@ Item { id: collectiblesDelegate CollectibleNestedDelegate { width: ListView.view.width + numItems: isCollection ? (!!communityId ? + root.collectibles.getNumberOfCollectiblesInCommunity(communityId) : + root.collectibles.getNumberOfCollectiblesInCollection(collectionUid)) : 0 onItemHovered: root.tokenHovered(selectedItem.uid, Constants.TokenType.ERC721, hovered) onItemSelected: { if (isCollection) { d.currentBrowsingCollectionName = collectionName - root.collectibles.currentCollectionUid = collectionUid + if (!!communityId) + root.collectibles.currentCollectionUid = communityId + else + root.collectibles.currentCollectionUid = collectionUid } else { root.tokenSelected(selectedItem.uid, Constants.TokenType.ERC721) } @@ -213,28 +245,26 @@ 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 = "" + onBackClicked: { + searchBox.reset() + root.collectibles.currentCollectionUid = "" + } } SearchBoxWithRightIcon { + id: searchBox + 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 {}