diff --git a/storybook/pages/SearchableAssetsPanelPage.qml b/storybook/pages/SearchableAssetsPanelPage.qml index 679a7a37f1..322a709f2d 100644 --- a/storybook/pages/SearchableAssetsPanelPage.qml +++ b/storybook/pages/SearchableAssetsPanelPage.qml @@ -75,6 +75,17 @@ Pane { iconSource: Constants.tokenIcon("ZRX"), balances: [], + sectionName: "Popular assets" + }, + { + tokensKey: "abc_key", + communityId: "", + name: "0x", + currencyBalanceAsString: "41,22 USD", + symbol: "ABC", + iconSource: Constants.tokenIcon("ABC"), + balances: [], + sectionName: "Popular assets" } ] diff --git a/storybook/pages/TokenSelectorAssetDelegatePage.qml b/storybook/pages/TokenSelectorAssetDelegatePage.qml index 375afafb48..77c17df12a 100644 --- a/storybook/pages/TokenSelectorAssetDelegatePage.qml +++ b/storybook/pages/TokenSelectorAssetDelegatePage.qml @@ -46,6 +46,7 @@ SplitView { symbol: "ETH" currencyBalanceAsString: "14,456.42 USD" iconSource: Constants.tokenIcon(symbol) + isAutoHovered: ctrlIsAutoHovered.checked balancesModel: ListModel { readonly property var data: [ @@ -85,6 +86,11 @@ SplitView { text: "Highlighted" checked: false } + Switch { + id: ctrlIsAutoHovered + text: "isAutoHovered" + checked: false + } Item { Layout.fillHeight: true } } diff --git a/storybook/pages/TokenSelectorCollectibleDelegatePage.qml b/storybook/pages/TokenSelectorCollectibleDelegatePage.qml index dd856aac82..4706a6935a 100644 --- a/storybook/pages/TokenSelectorCollectibleDelegatePage.qml +++ b/storybook/pages/TokenSelectorCollectibleDelegatePage.qml @@ -34,11 +34,13 @@ SplitView { name: nameTextField.text balance: balanceSpinBox.value ? balanceSpinBox.value : "" image: Constants.tokenIcon("ETH") + networkIcon: "network/Network=Ethereum" goDeeperIconVisible: goDeeperSwitch.checked interactive: interactiveSwitch.checked highlighted: highlightedSwitch.checked + isAutoHovered: ctrlIsAutoHovered.checked } } @@ -95,6 +97,12 @@ SplitView { checked: false } + Switch { + id: ctrlIsAutoHovered + text: "isAutoHovered" + checked: false + } + Item { Layout.fillHeight: true } } } diff --git a/storybook/pages/TokenSelectorPage.qml b/storybook/pages/TokenSelectorPage.qml index 7a90d0c348..a73daa9fb8 100644 --- a/storybook/pages/TokenSelectorPage.qml +++ b/storybook/pages/TokenSelectorPage.qml @@ -93,6 +93,8 @@ Pane { name: "My token", balance: 1, icon: Constants.tokenIcon("CFI"), + chainId: 11155111, + iconUrl: "network/Network=Ethereum" } ] }, @@ -106,18 +108,24 @@ Pane { name: "Furbeard", balance: 1, icon: Constants.tokenIcon("FUEL"), + chainId: 11155111, + iconUrl: "network/Network=Ethereum" }, { key: "collection_1_key_2", name: "Magicat", balance: 1, icon: Constants.tokenIcon("ENJ"), + chainId: 11155111, + iconUrl: "network/Network=Ethereum" }, { key: "collection_1_key_3", name: "Happy Meow", balance: 1, icon: Constants.tokenIcon("FUN"), + chainId: 11155111, + iconUrl: "network/Network=Ethereum" } ] }, @@ -130,19 +138,25 @@ Pane { key: "collection_2_key_1", name: "Unicorn 1", balance: 12, - icon: Constants.tokenIcon("CVC") + icon: Constants.tokenIcon("CVC"), + chainId: 11155111, + iconUrl: "network/Network=Ethereum" }, { key: "collection_2_key_2", name: "Unicorn 2", balance: 1, - icon: Constants.tokenIcon("CVC") + icon: Constants.tokenIcon("CVC"), + chainId: 11155111, + iconUrl: "network/Network=Ethereum" } ] }, { groupName: "Unicorn", icon: Constants.tokenIcon("ELF"), + chainId: 11155111, + iconUrl: "network/Network=Ethereum", type: "other", subitems: [ { diff --git a/storybook/pages/TokenSelectorPanelPage.qml b/storybook/pages/TokenSelectorPanelPage.qml index d4b0f9fa8b..01f7849e1d 100644 --- a/storybook/pages/TokenSelectorPanelPage.qml +++ b/storybook/pages/TokenSelectorPanelPage.qml @@ -92,6 +92,8 @@ Pane { name: "My token", balance: 1, icon: Constants.tokenIcon("CFI"), + chainId: 11155111, + iconUrl: "network/Network=Ethereum" } ] }, @@ -105,18 +107,24 @@ Pane { name: "Furbeard", balance: 1, icon: Constants.tokenIcon("FUEL"), + chainId: 11155111, + iconUrl: "network/Network=Ethereum" }, { key: "collection_1_key_2", name: "Magicat", balance: 1, icon: Constants.tokenIcon("ENJ"), + chainId: 11155111, + iconUrl: "network/Network=Ethereum" }, { key: "collection_1_key_3", name: "Happy Meow", balance: 1, icon: Constants.tokenIcon("FUN"), + chainId: 11155111, + iconUrl: "network/Network=Ethereum" } ] }, @@ -129,13 +137,17 @@ Pane { key: "collection_2_key_1", name: "Unicorn 1", balance: 12, - icon: Constants.tokenIcon("CVC") + icon: Constants.tokenIcon("CVC"), + chainId: 11155111, + iconUrl: "network/Network=Ethereum" }, { key: "collection_2_key_2", name: "Unicorn 2", balance: 1, - icon: Constants.tokenIcon("CVC") + icon: Constants.tokenIcon("CVC"), + chainId: 11155111, + iconUrl: "network/Network=Ethereum" } ] }, @@ -143,6 +155,8 @@ Pane { groupName: "Unicorn", icon: Constants.tokenIcon("ELF"), type: "other", + chainId: 11155111, + iconUrl: "network/Network=Ethereum", subitems: [ { key: "collection_3_key_1", diff --git a/storybook/qmlTests/tests/tst_SearchableAssetsPanel.qml b/storybook/qmlTests/tests/tst_SearchableAssetsPanel.qml index 0019b9fa89..c71c956f68 100644 --- a/storybook/qmlTests/tests/tst_SearchableAssetsPanel.qml +++ b/storybook/qmlTests/tests/tst_SearchableAssetsPanel.qml @@ -3,6 +3,8 @@ import QtTest 1.15 import AppLayouts.Wallet.panels 1.0 +import StatusQ.Core.Theme 0.1 + import Storybook 1.0 import utils 1.0 @@ -157,6 +159,8 @@ Item { const delegate1 = listView.itemAtIndex(0) verify(delegate1) compare(delegate1.name, "Status Test Token") + verify(delegate1.isAutoHovered) + compare(delegate1.background.color, Theme.palette.baseColor2) } { searchBox.text = "zrx" @@ -166,6 +170,8 @@ Item { const delegate1 = listView.itemAtIndex(0) verify(delegate1) compare(delegate1.name, "0x") + verify(delegate1.isAutoHovered) + compare(delegate1.background.color, Theme.palette.baseColor2) } { control.clearSearch() diff --git a/storybook/qmlTests/tests/tst_TokenSelectorAssetDelegate.qml b/storybook/qmlTests/tests/tst_TokenSelectorAssetDelegate.qml index 9ac42c8d6b..93ee179e65 100644 --- a/storybook/qmlTests/tests/tst_TokenSelectorAssetDelegate.qml +++ b/storybook/qmlTests/tests/tst_TokenSelectorAssetDelegate.qml @@ -5,6 +5,8 @@ import AppLayouts.Wallet.views 1.0 import Storybook 1.0 +import StatusQ.Core.Theme 0.1 + Item { id: root @@ -21,6 +23,7 @@ Item { symbol: "ETH" currencyBalanceAsString: "42.02 USD" iconSource: "" + isAutoHovered: false width: 250 readonly property SignalSpy clickSpy: SignalSpy { @@ -129,5 +132,27 @@ Item { verify(subBalanceText1.visible) verify(subBalanceText2.visible) } + + + function test_hovered_highlighted_states() { + const control = createTemporaryObject(delegateCmp, root, + { balancesModel }) + + control.highlighted = true + compare(control.background.color, Theme.palette.statusListItem.highlightColor) + + mouseMove(control, control.width/2, control.height/2) + compare(control.hovered, true) + compare(control.background.color, Theme.palette.baseColor2) + + control.highlighted = false + mouseMove(control, control.width/2, control.height/2) + compare(control.hovered, true) + compare(control.background.color, Theme.palette.baseColor2) + + // test isAutoHovered behaviour + control.isAutoHovered = true + compare(control.background.color, Theme.palette.baseColor2) + } } } diff --git a/storybook/qmlTests/tests/tst_TokenSelectorButton.qml b/storybook/qmlTests/tests/tst_TokenSelectorButton.qml index 58d937ecdf..d142f6a04b 100644 --- a/storybook/qmlTests/tests/tst_TokenSelectorButton.qml +++ b/storybook/qmlTests/tests/tst_TokenSelectorButton.qml @@ -5,6 +5,8 @@ import AppLayouts.Wallet.controls 1.0 import Storybook 1.0 +import StatusQ.Components 0.1 + Item { id: root @@ -39,6 +41,11 @@ Item { verify(!TestUtils.findTextItem(button, button.text)) verify(TestUtils.findTextItem(button, "ETH")) verify(findChild(button, "selectedContent")) + + const icon = TestUtils.findByType(button, StatusRoundedImage) + verify(icon) + compare(icon.width, 24) + compare(icon.height, 24) } } } diff --git a/ui/app/AppLayouts/Wallet/adaptors/CollectiblesSelectionAdaptor.qml b/ui/app/AppLayouts/Wallet/adaptors/CollectiblesSelectionAdaptor.qml index 654e536cf2..2bc4fb9ca3 100644 --- a/ui/app/AppLayouts/Wallet/adaptors/CollectiblesSelectionAdaptor.qml +++ b/ui/app/AppLayouts/Wallet/adaptors/CollectiblesSelectionAdaptor.qml @@ -32,6 +32,16 @@ QObject { /** Account key used for filtering **/ property string accountKey + + /** + Expected model structure: + + chainId [int] - network chain id + chainName [string] - name of network + iconUrl [string] - network icon url + **/ + property var networksModel + /** Expected model structure: @@ -64,13 +74,20 @@ QObject { **/ readonly property alias model: communityGroupsGrouppedByCollection + LeftJoinModel { + id: jointCollectiblesByNwChainId + leftModel: collectiblesModel ?? null + rightModel: networksModel + joinRole: "chainId" + } + SortFilterProxyModel { id: initiallyFilteredAndSorted objectName: "collectiblesSelectionAdaptor_initiallyFilteredAndSorted" sourceModel: ObjectProxyModel { - sourceModel: collectiblesModel ?? null + sourceModel: jointCollectiblesByNwChainId delegate: QObject { readonly property int balance: balanceAggregator.value /* 3 */ diff --git a/ui/app/AppLayouts/Wallet/controls/TokenSelector.qml b/ui/app/AppLayouts/Wallet/controls/TokenSelector.qml index 1503138855..ccbb79c9e0 100644 --- a/ui/app/AppLayouts/Wallet/controls/TokenSelector.qml +++ b/ui/app/AppLayouts/Wallet/controls/TokenSelector.qml @@ -34,6 +34,11 @@ Control { tokenSelectorPanel.highlightedKey = key ?? "" } + QObject { + id: d + readonly property int maxPopupHeight: 330 + } + contentItem: TokenSelectorButton { id: tokenSelectorButton @@ -52,54 +57,63 @@ Control { horizontalPadding: 0 bottomPadding: 0 - contentItem: TokenSelectorPanel { - id: tokenSelectorPanel + onClosed: tokenSelectorPanel.clear() - objectName: "tokenSelectorPanel" + contentItem: Item { - function findSubitem(key) { - const count = collectiblesModel.rowCount() + implicitHeight: Math.min(tokenSelectorPanel.implicitHeight, d.maxPopupHeight) - for (let i = 0; i < count; i++) { - const entry = ModelUtils.get(collectiblesModel, i) - const subitem = ModelUtils.getByKey( - entry.subitems, "key", key) - if (subitem) - return subitem + TokenSelectorPanel { + id: tokenSelectorPanel + + objectName: "tokenSelectorPanel" + + anchors.fill: parent + + function findSubitem(key) { + const count = collectiblesModel.rowCount() + + for (let i = 0; i < count; i++) { + const entry = ModelUtils.get(collectiblesModel, i) + const subitem = ModelUtils.getByKey( + entry.subitems, "key", key) + if (subitem) + return subitem + } } - } - function setCurrentAndClose(name, icon) { - tokenSelectorButton.name = name - tokenSelectorButton.icon = icon - tokenSelectorButton.selected = true - dropdown.close() - } + function setCurrentAndClose(name, icon) { + tokenSelectorButton.name = name + tokenSelectorButton.icon = icon + tokenSelectorButton.selected = true + dropdown.close() + } - onAssetSelected: { - const entry = ModelUtils.getByKey(assetsModel, "tokensKey", key) - highlightedKey = key + onAssetSelected: { + const entry = ModelUtils.getByKey(assetsModel, "tokensKey", key) + highlightedKey = key - setCurrentAndClose(entry.symbol, entry.iconSource) - root.assetSelected(key) - } + setCurrentAndClose(entry.symbol, entry.iconSource) + root.assetSelected(key) + } - onCollectibleSelected: { - highlightedKey = key + onCollectibleSelected: { + highlightedKey = key - const subitem = findSubitem(key) - setCurrentAndClose(subitem.name, subitem.icon) + const subitem = findSubitem(key) + setCurrentAndClose(subitem.name, subitem.icon) - root.collectibleSelected(key) - } + root.collectibleSelected(key) + } - onCollectionSelected: { - highlightedKey = key + onCollectionSelected: { + highlightedKey = key - const subitem = findSubitem(key) - setCurrentAndClose(subitem.name, subitem.icon) + const subitem = findSubitem(key) + setCurrentAndClose(subitem.name, subitem.icon) - root.collectionSelected(key) + root.collectionSelected(key) + } } } } diff --git a/ui/app/AppLayouts/Wallet/controls/TokenSelectorButton.qml b/ui/app/AppLayouts/Wallet/controls/TokenSelectorButton.qml index f5be649407..3f26c28ffa 100644 --- a/ui/app/AppLayouts/Wallet/controls/TokenSelectorButton.qml +++ b/ui/app/AppLayouts/Wallet/controls/TokenSelectorButton.qml @@ -74,8 +74,8 @@ Control { id: tokenSelectorIcon objectName: "tokenSelectorIcon" - Layout.preferredWidth: 21 - Layout.preferredHeight: 21 + Layout.preferredWidth: 24 + Layout.preferredHeight: 24 image.source: root.icon } diff --git a/ui/app/AppLayouts/Wallet/panels/SearchableAssetsPanel.qml b/ui/app/AppLayouts/Wallet/panels/SearchableAssetsPanel.qml index d783be3483..77e2aa45f7 100644 --- a/ui/app/AppLayouts/Wallet/panels/SearchableAssetsPanel.qml +++ b/ui/app/AppLayouts/Wallet/panels/SearchableAssetsPanel.qml @@ -39,6 +39,11 @@ Control { searchBox.text = "" } + QtObject { + id: d + readonly property bool validSearchResultExists: !!searchBox.text && sfpm.rowCount() > 0 + } + SortFilterProxyModel { id: sfpm @@ -64,6 +69,8 @@ Control { Layout.fillWidth: true placeholderText: qsTr("Search assets") + + Keys.forwardTo: [listView] } StatusDialogDivider { @@ -81,6 +88,10 @@ Control { Layout.fillWidth: true Layout.fillHeight: true Layout.preferredHeight: contentHeight + Layout.leftMargin: 4 + Layout.rightMargin: 4 + + spacing: 4 model: sfpm section.property: "sectionName" @@ -99,6 +110,7 @@ Control { highlighted: model.tokensKey === root.highlightedKey enabled: model.tokensKey !== root.nonInteractiveKey balancesListInteractive: !ListView.view.moving + isAutoHovered: d.validSearchResultExists && index === 0 && !listViewHoverHandler.hovered name: model.name symbol: model.symbol @@ -108,6 +120,15 @@ Control { onClicked: root.selected(model.tokensKey) } + + Keys.onReturnPressed: { + if(d.validSearchResultExists) + listView.itemAtIndex(0).clicked() + } + + HoverHandler { + id: listViewHoverHandler + } } } } diff --git a/ui/app/AppLayouts/Wallet/panels/SearchableCollectiblesPanel.qml b/ui/app/AppLayouts/Wallet/panels/SearchableCollectiblesPanel.qml index 70532e02eb..390d7a25a5 100644 --- a/ui/app/AppLayouts/Wallet/panels/SearchableCollectiblesPanel.qml +++ b/ui/app/AppLayouts/Wallet/panels/SearchableCollectiblesPanel.qml @@ -42,6 +42,10 @@ Control { readonly property alias currentItem: collectiblesStackView.currentItem + function clearSearch() { + collectiblesSearchBox.text = "" + } + SortFilterProxyModel { id: sfpm @@ -59,6 +63,14 @@ Control { initialItem: ColumnLayout { spacing: 0 + StatusBaseText { + Layout.alignment: Qt.AlignHCenter + Layout.bottomMargin: 4 + text: qsTr("Your collectibles will appear here") + color: Theme.palette.baseColor1 + visible: !collectiblesListView.count && !collectiblesSearchBox.text + } + TokenSearchBox { id: collectiblesSearchBox @@ -66,6 +78,10 @@ Control { Layout.fillWidth: true placeholderText: qsTr("Search collectibles") + + visible: collectiblesListView.count || !!collectiblesSearchBox.text + + Keys.forwardTo: [collectiblesListView] } StatusDialogDivider { @@ -76,9 +92,15 @@ Control { StatusListView { id: collectiblesListView + readonly property bool validSearchResultExists: !!collectiblesSearchBox.text && sfpm.count > 0 + Layout.fillWidth: true Layout.fillHeight: true Layout.preferredHeight: contentHeight + Layout.leftMargin: 4 + Layout.rightMargin: 4 + + spacing: 4 clip: true model: sfpm @@ -100,10 +122,12 @@ Control { image: model.icon goDeeperIconVisible: subitemsCount > 1 || isCommunity + networkIcon: model.iconUrl highlighted: subitemsCount === 1 && !isCommunity ? ModelUtils.get(model.subitems, 0, "key") === root.highlightedKey : false + isAutoHovered: collectiblesListView.validSearchResultExists && model.index === 0 && !collectiblesListViewHoverHandler.hovered onClicked: { if (subitemsCount === 1 && !isCommunity) { @@ -133,6 +157,15 @@ Control { ? qsTr("Community minted") : qsTr("Other") } + + Keys.onReturnPressed: { + if(validSearchResultExists) + collectiblesListView.itemAtIndex(0).clicked() + } + + HoverHandler { + id: collectiblesListViewHoverHandler + } } } } @@ -180,6 +213,8 @@ Control { Layout.fillWidth: true placeholderText: qsTr("Search collectibles") + + Keys.forwardTo: [sublist] } StatusDialogDivider { @@ -190,9 +225,13 @@ Control { StatusListView { id: sublist + readonly property bool validSearchResultExists: !!collectiblesSublistSearchBox.text && sublistSfpm.rowCount() > 0 + Layout.fillWidth: true Layout.fillHeight: true Layout.preferredHeight: contentHeight + Layout.leftMargin: 4 + Layout.rightMargin: 4 model: sublistSfpm @@ -205,7 +244,9 @@ Control { balance: model.balance > 1 ? model.balance : "" image: model.icon goDeeperIconVisible: false + networkIcon: model.iconUrl highlighted: model.key === root.highlightedKey + isAutoHovered: sublist.validSearchResultExists && model.index === 0 && !sublistHoverHandler.hovered onClicked: { if (isCommunity) @@ -214,6 +255,15 @@ Control { root.collectibleSelected(model.key) } } + + Keys.onReturnPressed: { + if(validSearchResultExists) + sublist.itemAtIndex(0).clicked() + } + + HoverHandler { + id: sublistHoverHandler + } } // Detection if the related model entry has been removed. diff --git a/ui/app/AppLayouts/Wallet/panels/TokenSelectorPanel.qml b/ui/app/AppLayouts/Wallet/panels/TokenSelectorPanel.qml index 7e3a3351a5..bcd4b56064 100644 --- a/ui/app/AppLayouts/Wallet/panels/TokenSelectorPanel.qml +++ b/ui/app/AppLayouts/Wallet/panels/TokenSelectorPanel.qml @@ -40,6 +40,11 @@ Control { property string highlightedKey: "" + function clear() { + searchableAssetsPanel.clearSearch() + searchableCollectiblesPanel.clearSearch() + } + contentItem: ColumnLayout { StatusTabBar { id: tabBar diff --git a/ui/app/AppLayouts/Wallet/views/TokenSelectorAssetDelegate.qml b/ui/app/AppLayouts/Wallet/views/TokenSelectorAssetDelegate.qml index 3027584316..ce9b896e97 100644 --- a/ui/app/AppLayouts/Wallet/views/TokenSelectorAssetDelegate.qml +++ b/ui/app/AppLayouts/Wallet/views/TokenSelectorAssetDelegate.qml @@ -17,6 +17,7 @@ ItemDelegate { required property string symbol required property string currencyBalanceAsString required property string iconSource + required property bool isAutoHovered // expected structure: [iconUrl: string, balanceAsString: string] property alias balancesModel: balancesListView.model @@ -36,9 +37,11 @@ ItemDelegate { background: Rectangle { radius: Theme.radius - color: root.hovered || root.highlighted - ? Theme.palette.statusListItem.highlightColor - : "transparent" + color: root.hovered || root.isAutoHovered + ? Theme.palette.baseColor2 + : root.highlighted + ? Theme.palette.statusListItem.highlightColor + : "transparent" HoverHandler { cursorShape: root.enabled ? Qt.PointingHandCursor : undefined diff --git a/ui/app/AppLayouts/Wallet/views/TokenSelectorCollectibleDelegate.qml b/ui/app/AppLayouts/Wallet/views/TokenSelectorCollectibleDelegate.qml index a7508d58a9..6eedcf052c 100644 --- a/ui/app/AppLayouts/Wallet/views/TokenSelectorCollectibleDelegate.qml +++ b/ui/app/AppLayouts/Wallet/views/TokenSelectorCollectibleDelegate.qml @@ -15,6 +15,8 @@ ItemDelegate { required property string name required property string balance required property url image + required property string networkIcon + required property bool isAutoHovered property bool goDeeperIconVisible: true property bool interactive: true @@ -36,9 +38,11 @@ ItemDelegate { background: Rectangle { radius: Theme.radius - color: (root.interactive && root.hovered) || root.highlighted - ? Theme.palette.statusListItem.highlightColor - : "transparent" + color: (root.interactive && (root.hovered || isAutoHovered )) + ? Theme.palette.baseColor2 + : root.highlighted + ? Theme.palette.statusListItem.highlightColor + : "transparent" HoverHandler { cursorShape: root.interactive ? Qt.PointingHandCursor : undefined @@ -59,7 +63,7 @@ ItemDelegate { Layout.fillWidth: true spacing: 0 - // name, symbol, total balance + // name, symbol, total balance, network icon RowLayout { Layout.fillWidth: true spacing: root.spacing @@ -86,6 +90,15 @@ ItemDelegate { elide: Text.ElideRight } + StatusRoundedImage { + Layout.alignment: Qt.AlignVCenter + Layout.preferredWidth: 20 + Layout.preferredHeight: 20 + + image.source: Theme.svg("tiny/%1".arg(root.networkIcon)) + visible:(root.hovered || isAutoHovered) && !root.goDeeperIconVisible + } + StatusIcon { Layout.alignment: Qt.AlignVCenter diff --git a/ui/imports/shared/popups/send/SendModal.qml b/ui/imports/shared/popups/send/SendModal.qml index ffb8ce77c9..3e4a997608 100644 --- a/ui/imports/shared/popups/send/SendModal.qml +++ b/ui/imports/shared/popups/send/SendModal.qml @@ -409,6 +409,10 @@ StatusDialog { sourceComponent: CollectiblesSelectionAdaptor { accountKey: popup.selectedAccount.address + networksModel: SortFilterProxyModel { + sourceModel: popup.store.flatNetworksModel + filters: ValueFilter { roleName: "isTest"; value: popup.store.areTestNetworksEnabled } + } collectiblesModel: SortFilterProxyModel { sourceModel: collectiblesStore ? collectiblesStore.jointCollectiblesBySymbolModel : null filters: ValueFilter {