From 901362dfc19392a4b58db8e54df7aa52c66bd555 Mon Sep 17 00:00:00 2001 From: Alex Jbanca Date: Sun, 16 Jun 2024 00:33:12 +0300 Subject: [PATCH] feat(networkSelector): Refatoring of NetworkSelector to remove backend dependency --- .../main/wallet_section/networks/model.nim | 4 +- .../main/wallet_section/networks/view.nim | 2 +- storybook/pages/AccountViewPage.qml | 12 -- .../pages/AddEditSavedAddressPopupPage.qml | 8 +- storybook/pages/EditOwnerTokenViewPage.qml | 1 + storybook/pages/NetworkFilterPage.qml | 117 ++--------- storybook/pages/NetworkSelectPopupPage.qml | 161 +++++---------- storybook/pages/ReceiveModalPage.qml | 29 +-- storybook/pages/WalletHeaderPage.qml | 1 + .../qmlTests/tests/tst_NetworkSelectPopup.qml | 89 +++++++++ .../tests/tst_NetworkSelectorView.qml | 2 +- storybook/src/Models/NetworksModel.qml | 4 +- .../src/StatusQ/Controls/StatusComboBox.qml | 1 + .../Communities/views/EditOwnerTokenView.qml | 25 +-- .../AppLayouts/Profile/stores/WalletStore.qml | 23 --- .../Profile/views/wallet/AccountView.qml | 20 +- .../Wallet/controls/NetworkFilter.qml | 189 +++++++++--------- .../controls/NetworkSelectItemDelegate.qml | 2 +- .../Wallet/controls/StatusNetworkSelector.qml | 2 +- .../Wallet/panels/DAppsWorkflow.qml | 2 +- .../AppLayouts/Wallet/panels/WalletHeader.qml | 25 ++- .../popups/AddEditSavedAddressPopup.qml | 124 ++++++------ .../Wallet/popups/NetworkSelectPopup.qml | 69 +++---- .../AppLayouts/Wallet/popups/ReceiveModal.qml | 29 ++- .../Wallet/popups/swap/SwapModal.qml | 15 +- .../SingleSelectionInfo.qml | 8 - ui/app/AppLayouts/Wallet/stores/RootStore.qml | 23 --- .../Wallet/views/NetworkSelectorView.qml | 6 +- .../icons/network/Network=Arbitrum-test.svg | 15 ++ .../icons/network/Network=Ethereum-test.svg | 9 + .../icons/network/Network=Optimism-test.svg | 5 + .../popups/walletconnect/ConnectDAppModal.qml | 26 ++- 32 files changed, 491 insertions(+), 557 deletions(-) create mode 100644 storybook/qmlTests/tests/tst_NetworkSelectPopup.qml delete mode 100644 ui/app/AppLayouts/Wallet/stores/NetworkSelectPopup/SingleSelectionInfo.qml create mode 100644 ui/imports/assets/icons/network/Network=Arbitrum-test.svg create mode 100644 ui/imports/assets/icons/network/Network=Ethereum-test.svg create mode 100644 ui/imports/assets/icons/network/Network=Optimism-test.svg diff --git a/src/app/modules/main/wallet_section/networks/model.nim b/src/app/modules/main/wallet_section/networks/model.nim index 2c2dc480cd..6acab13c92 100644 --- a/src/app/modules/main/wallet_section/networks/model.nim +++ b/src/app/modules/main/wallet_section/networks/model.nim @@ -180,12 +180,12 @@ QtObject: break return networkString - proc getNetworkIds*(self: Model, shortNames: string): string = + proc getNetworkIds*(self: Model, shortNames: string, areTestNetworksEnabled: bool): string = var networkIds = "" let networksNames = shortNames.split(":") for name in networksNames: for item in self.delegate.getFlatNetworksList(): - if item.shortName == name: + if item.shortName == name and item.isTest == areTestNetworksEnabled: networkIds = networkIds & $item.chainId & ':' break return networkIds diff --git a/src/app/modules/main/wallet_section/networks/view.nim b/src/app/modules/main/wallet_section/networks/view.nim index 4a4df17499..d45e4794bc 100644 --- a/src/app/modules/main/wallet_section/networks/view.nim +++ b/src/app/modules/main/wallet_section/networks/view.nim @@ -95,7 +95,7 @@ QtObject: return self.flatNetworks.getNetworkShortNames(preferredNetworks, self.areTestNetworksEnabled) proc getNetworkIds*(self: View, shortNames: string): string {.slot.} = - return self.flatNetworks.getNetworkIds(shortNames) + return self.flatNetworks.getNetworkIds(shortNames, self.areTestNetworksEnabled) proc getBlockExplorerURL*(self: View, chainId: int): string {.slot.} = return self.flatNetworks.getBlockExplorerURL(chainId) diff --git a/storybook/pages/AccountViewPage.qml b/storybook/pages/AccountViewPage.qml index 39d8516b3a..53c6e5d3d1 100644 --- a/storybook/pages/AccountViewPage.qml +++ b/storybook/pages/AccountViewPage.qml @@ -54,7 +54,6 @@ SplitView { sourceModel: NetworksModel.flatNetworks filters: ValueFilter { roleName: "isTest"; value: areTestNetworksEnabledCheckbox.checked } } - property var filteredFlatModel: networks property bool areTestNetworksEnabled: areTestNetworksEnabledCheckbox.checked function toggleNetwork(chainId) { @@ -71,17 +70,6 @@ SplitView { function updateWalletAccountPreferredChains(address, preferredChainIds) { console.warn("updateWalletAccountPreferredChains :: address ::", address, "preferredChainIds :: ", preferredChainIds) } - - function processPreferredSharingNetworkToggle(preferredSharingNetworksArray, network) { - console.warn("processPreferredSharingNetworkToggle :: preferredSharingNetworksArray ::", preferredSharingNetworksArray, "network :: ", network) - const chainId = network.chainId.toString() - if (preferredSharingNetworksArray.includes(chainId)) { - preferredSharingNetworksArray.splice(preferredSharingNetworksArray.indexOf(chainId), 1) - } else { - preferredSharingNetworksArray.push(chainId) - } - return [...preferredSharingNetworksArray] - } } property var keyPairModel: WalletKeyPairModel {} diff --git a/storybook/pages/AddEditSavedAddressPopupPage.qml b/storybook/pages/AddEditSavedAddressPopupPage.qml index be260b4c00..9001deb6f9 100644 --- a/storybook/pages/AddEditSavedAddressPopupPage.qml +++ b/storybook/pages/AddEditSavedAddressPopupPage.qml @@ -2,6 +2,7 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 +import StatusQ.Core.Utils 0.1 import SortFilterProxyModel 0.2 import Storybook 1.0 @@ -42,7 +43,7 @@ SplitView { destroyOnClose: true modal: false closePolicy: Popup.NoAutoClose - + flatNetworks: SortFilterProxyModel { sourceModel: NetworksModel.flatNetworks filters: ValueFilter { roleName: "isTest"; value: false } @@ -55,6 +56,11 @@ SplitView { function createOrUpdateSavedAddress(name, address, ens, colorId, chainShortNames) { logs.logEvent("createOrUpdateSavedAddress", ["name", "address", "ens", "colorId", "chainShortNames"], arguments) } + function getNetworkIds(chainSortNames) { + let shortNames = chainSortNames.split(":").filter((shortName) => shortName.length > 0) + const chainIds = shortNames.map((shortName) => ModelUtils.getByKey(NetworksModel.flatNetworks, "shortName", shortName).chainId) + return chainIds.join(":") + } } // Emulate resolving ENS by simple validation diff --git a/storybook/pages/EditOwnerTokenViewPage.qml b/storybook/pages/EditOwnerTokenViewPage.qml index 56d81e808b..97cacc0eff 100644 --- a/storybook/pages/EditOwnerTokenViewPage.qml +++ b/storybook/pages/EditOwnerTokenViewPage.qml @@ -46,6 +46,7 @@ SplitView { communityName: communityName.text communityLogo: doodles.checked ? ModelsData.collectibles.doodles : ModelsData.collectibles.mana communityColor: color1.checked ? "#FFC4E9" : "#f44336" + ownerToken.chainId: 42161 flatNetworks: SortFilterProxyModel { sourceModel: NetworksModel.flatNetworks diff --git a/storybook/pages/NetworkFilterPage.qml b/storybook/pages/NetworkFilterPage.qml index 927509a4a2..893895c8b4 100644 --- a/storybook/pages/NetworkFilterPage.qml +++ b/storybook/pages/NetworkFilterPage.qml @@ -15,83 +15,10 @@ SplitView { id: root Logs { id: logs } - readonly property string ethereumName : "Ethereum Mainnet" + readonly property string ethereumName : "Mainnet" readonly property string optimismName : "Optimism" readonly property string arbitrumName : "Arbitrum" - - - // Keep a clone so that the UX can be modified without affecting the original model - CloneModel { - id: simulatedNimModel - - sourceModel: SortFilterProxyModel { - sourceModel: NetworksModel.flatNetworks - filters: ValueFilter { roleName: "isTest"; value: false } - } - - roles: ["chainId", "layer", "chainName", "isTest", "isEnabled", "iconUrl", "shortName", "chainColor"] - rolesOverride: [{ role: "enabledState", transform: (mD) => { - return simulatedNimModel.areAllEnabled(sourceModel) - ? NetworkSelectionView.UxEnabledState.AllEnabled - : mD.isEnabled - ? NetworkSelectionView.UxEnabledState.Enabled - : NetworkSelectionView.UxEnabledState.Disabled - } - }] - - /// Simulate the Nim model - function toggleNetwork(network) { - const chainId = network.chainId - let chainIdOnlyEnabled = true - let chainIdOnlyDisabled = true - let allEnabled = true - for (let i = 0; i < simulatedNimModel.count; i++) { - const item = simulatedNimModel.get(i) - if(item.enabledState === NetworkSelectionView.UxEnabledState.Enabled) { - if(item.chainId !== chainId) { - chainIdOnlyEnabled = false - } - } else if(item.enabledState === NetworkSelectionView.UxEnabledState.Disabled) { - if(item.chainId !== chainId) { - chainIdOnlyDisabled = false - } - allEnabled = false - } else { - if(item.chainId === chainId) { - chainIdOnlyDisabled = false - chainIdOnlyEnabled = false - } - } - } - for (let i = 0; i < simulatedNimModel.count; i++) { - const item = simulatedNimModel.get(i) - if(allEnabled) { - simulatedNimModel.setProperty(i, "enabledState", item.chainId === chainId ? NetworkSelectionView.UxEnabledState.Enabled : NetworkSelectionView.UxEnabledState.Disabled) - } else if(chainIdOnlyEnabled || chainIdOnlyDisabled) { - simulatedNimModel.setProperty(i, "enabledState", NetworkSelectionView.UxEnabledState.AllEnabled) - } else if(item.chainId === chainId) { - simulatedNimModel.setProperty(i, "enabledState", item.enabledState === NetworkSelectionView.UxEnabledState.Enabled - ? NetworkSelectionView.UxEnabledState.Disabled - : NetworkSelectionView.UxEnabledState.Enabled) - } - const haveEnabled = item.enabledState !== NetworkSelectionView.UxEnabledState.Disabled - if(item.isEnabled !== haveEnabled) { - simulatedNimModel.setProperty(i, "isEnabled", haveEnabled) - } - } - } - - function areAllEnabled(modelToCheck) { - for (let i = 0; i < modelToCheck.count; i++) { - if(!(modelToCheck.get(i).isEnabled)) { - return false - } - } - return true - } - } - SplitView { orientation: Qt.Vertical SplitView.fillWidth: true @@ -107,30 +34,13 @@ SplitView { anchors.centerIn: parent - flatNetworks: simulatedNimModel + flatNetworks: NetworksModel.flatNetworks multiSelection: multiSelectionCheckBox.checked showAllSelectedText: ctrlShowAllSelectedText.checked showTitle: ctrlShowTitle.checked - showCheckboxes: ctrlShowCheckBoxes.checked - showRadioButtons: ctrlShowRadioButtons.checked - - onToggleNetwork: (network) => { - logs.logEvent("onToggleNetwork: " + network.chainName) - - if(multiSelection) { - simulatedNimModel.toggleNetwork(network) - } else { - if(network.chainName === root.ethereumName) - ethRadioBtn.checked = true - - else if(network.chainName === root.optimismName) - optRadioBtn.checked = true - - else if(network.chainName === root.arbitrumName) - arbRadioBtn.checked = true - } - } + selectionAllowed: selectionAllowedCheckBox.checked + showSelectionIndicator: (ctrlShowCheckBoxes.checked && multiSelection) || (ctrlShowRadioButtons.checked && !multiSelection) } } @@ -154,13 +64,11 @@ SplitView { CheckBox { id: multiSelectionCheckBox text: "Multi selection" - checked: true - onCheckedChanged: if(!checked) ethRadioBtn.checked = true + checked: false } CheckBox { id: ctrlShowTitle - visible: !multiSelectionCheckBox.checked text: "Show title text" checked: true } @@ -186,6 +94,12 @@ SplitView { checked: true } + CheckBox { + id: selectionAllowedCheckBox + text: "Selection allowed" + checked: true + } + ColumnLayout { visible: !multiSelectionCheckBox.checked Label { @@ -197,19 +111,22 @@ SplitView { id: ethRadioBtn text: root.ethereumName - onCheckedChanged: if(checked) networkFilter.setChain(NetworksModel.ethNet) + checked: networkFilter.selection.includes(NetworksModel.ethNet) + onToggled: networkFilter.selection = [NetworksModel.ethNet] } RadioButton { id: optRadioBtn text: root.optimismName - onCheckedChanged: if(checked) networkFilter.setChain(NetworksModel.optimismNet) + checked: networkFilter.selection.includes(NetworksModel.optimismNet) + onToggled: networkFilter.selection = [NetworksModel.optimismNet] } RadioButton { id: arbRadioBtn text: root.arbitrumName - onCheckedChanged: if(checked) networkFilter.setChain(NetworksModel.arbitrumNet) + checked: networkFilter.selection.includes(NetworksModel.arbitrumNet) + onToggled: networkFilter.selection = [NetworksModel.arbitrumNet] } } } diff --git a/storybook/pages/NetworkSelectPopupPage.qml b/storybook/pages/NetworkSelectPopupPage.qml index 01239758be..d6e3c2059a 100644 --- a/storybook/pages/NetworkSelectPopupPage.qml +++ b/storybook/pages/NetworkSelectPopupPage.qml @@ -2,6 +2,7 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 +import StatusQ 0.1 import StatusQ.Core 0.1 import StatusQ.Core.Theme 0.1 import StatusQ.Popups 0.1 @@ -39,16 +40,7 @@ SplitView { Layout.alignment: Qt.AlignHCenter - flatNetworks: simulatedNimModel - - onToggleNetwork: (network) => { - if(multiSelection) { - simulatedNimModel.toggleNetwork(network) - } else { - lastSingleSelectionLabel.text = `[${network.chainName}] (NL) - ID: ${network.chainId}, Icon: ${network.iconUrl}` - } - } - + flatNetworks: availableNetworks multiSelection: multiSelectionCheckbox.checked } @@ -56,21 +48,15 @@ SplitView { Item { id: popupPlaceholder - Layout.preferredWidth: networkSelectPopup.width - Layout.preferredHeight: networkSelectPopup.height + Layout.preferredWidth: networkSelectPopup.implicitWidth + Layout.preferredHeight: networkSelectPopup.implicitHeight NetworkSelectPopup { id: networkSelectPopup - - flatNetworks: simulatedNimModel - - useEnabledRole: false - - visible: true + flatNetworks: availableNetworks + multiSelection: multiSelectionCheckbox.checked closePolicy: Popup.NoAutoClose - - // Simulates a network toggle - onToggleNetwork: (network, index) => simulatedNimModel.toggleNetwork(network) + visible: true } } @@ -86,15 +72,21 @@ SplitView { } Label { id: lastSingleSelectionLabel - text: "-" + text: selectedEntry.available ? `[${selectedEntry.item.chainName}] - ID: ${selectedEntry.item.chainId}, Icon: ${selectedEntry.item.iconUrl}` : "-" + } + + ModelEntry { + id: selectedEntry + sourceModel: availableNetworks + key: "chainId" } } Item { id: singleSelectionPopupPlaceholder - Layout.preferredWidth: selectPopupLoader.item ? selectPopupLoader.item.width : 0 - Layout.preferredHeight: selectPopupLoader.item ? selectPopupLoader.item.height : 0 + Layout.preferredWidth: selectPopupLoader.item ? selectPopupLoader.item.implicitWidth : 0 + Layout.preferredHeight: selectPopupLoader.item ? selectPopupLoader.item.implicitHeight : 0 property var currentModel: networkFilter.flatNetworks property int currentIndex: 0 @@ -105,20 +97,15 @@ SplitView { active: false sourceComponent: NetworkSelectPopup { - flatNetworks: simulatedNimModel - - singleSelection { - enabled: true - currentModel: singleSelectionPopupPlaceholder.currentModel - currentIndex: singleSelectionPopupPlaceholder.currentIndex - } - - onToggleNetwork: (network, index) => { - lastSingleSelectionLabel.text = `[${network.chainName}] - ID: ${network.chainId}, Icon: ${network.iconUrl}` - singleSelectionPopupPlaceholder.currentIndex = index - } - + flatNetworks: availableNetworks + selection: selectedEntry.available ? [selectedEntry.value] : [] onClosed: selectPopupLoader.active = false + + onSelectionChanged: { + if (selectedEntry.value !== selection[0]) { + selectedEntry.value = selection[0] + } + } } onLoaded: item.open() @@ -143,9 +130,11 @@ SplitView { Layout.fillWidth: true Layout.fillHeight: true - model: simulatedNimModel + model: availableNetworks delegate: ItemDelegate { + required property var model + width: allNetworksListView.width implicitHeight: delegateRowLayout.implicitHeight @@ -167,20 +156,20 @@ SplitView { Label { text: `${model.shortName}` } Label { text: `ID ${model.chainId}` } CheckBox { - checkState: model.isEnabled ? Qt.Checked : Qt.Unchecked - tristate: true - nextCheckState: () => { - const nextEnabled = (checkState !== Qt.Checked) - availableNetworks.sourceModel.setProperty(availableNetworks.mapToSource(index), "isEnabled", nextEnabled) - Qt.callLater(() => { simulatedNimModel.cloneModel(availableNetworks) }) - return nextEnabled ? Qt.Checked : Qt.Unchecked + checkState: networkSelectPopup.selection.includes(model.chainId) ? Qt.Checked : Qt.Unchecked + onToggled: { + let currentSelection = networkSelectPopup.selection + if (checkState === Qt.Checked && !currentSelection.includes(model.chainId)) { + currentSelection.push(model.chainId) + } else { + currentSelection = currentSelection.filter(id => id !== model.chainId) + } + networkSelectPopup.selection = [...currentSelection] } } } } } - - onClicked: allNetworksListView.currentIndex = index } } CheckBox { @@ -199,7 +188,15 @@ SplitView { text: "Test Networks Mode" checked: false - onCheckedChanged: Qt.callLater(simulatedNimModel.cloneModel, availableNetworks) + } + + CheckBox { + id: allowSelection + Layout.margins: 5 + + text: "Allow Selection" + checked: networkSelectPopup.selectionAllowed + onToggled: networkSelectPopup.selectionAllowed = checked } } } @@ -210,74 +207,6 @@ SplitView { sourceModel: NetworksModel.flatNetworks filters: ValueFilter { roleName: "isTest"; value: testModeCheckbox.checked; } } - - // Keep a clone so that the UX can be modified without affecting the original model - CloneModel { - id: simulatedNimModel - - sourceModel: availableNetworks - - roles: ["chainId", "layer", "chainName", "isTest", "isEnabled", "iconUrl", "shortName", "chainColor"] - rolesOverride: [{ role: "enabledState", transform: (mD) => { - return simulatedNimModel.areAllEnabled(sourceModel) - ? NetworkSelectionView.UxEnabledState.AllEnabled - : mD.isEnabled - ? NetworkSelectionView.UxEnabledState.Enabled - : NetworkSelectionView.UxEnabledState.Disabled - } - }] - - /// Simulate the Nim model - function toggleNetwork(network) { - const chainId = network.chainId - let chainIdOnlyEnabled = true - let chainIdOnlyDisabled = true - let allEnabled = true - for (let i = 0; i < simulatedNimModel.count; i++) { - const item = simulatedNimModel.get(i) - if(item.enabledState === NetworkSelectionView.UxEnabledState.Enabled) { - if(item.chainId !== chainId) { - chainIdOnlyEnabled = false - } - } else if(item.enabledState === NetworkSelectionView.UxEnabledState.Disabled) { - if(item.chainId !== chainId) { - chainIdOnlyDisabled = false - } - allEnabled = false - } else { - if(item.chainId === chainId) { - chainIdOnlyDisabled = false - chainIdOnlyEnabled = false - } - } - } - for (let i = 0; i < simulatedNimModel.count; i++) { - const item = simulatedNimModel.get(i) - if(allEnabled) { - simulatedNimModel.setProperty(i, "enabledState", item.chainId === chainId ? NetworkSelectionView.UxEnabledState.Enabled : NetworkSelectionView.UxEnabledState.Disabled) - } else if(chainIdOnlyEnabled || chainIdOnlyDisabled) { - simulatedNimModel.setProperty(i, "enabledState", NetworkSelectionView.UxEnabledState.AllEnabled) - } else if(item.chainId === chainId) { - simulatedNimModel.setProperty(i, "enabledState", item.enabledState === NetworkSelectionView.UxEnabledState.Enabled - ? NetworkSelectionView.UxEnabledState.Disabled - :NetworkSelectionView.UxEnabledState.Enabled) - } - const haveEnabled = item.enabledState !== NetworkSelectionView.UxEnabledState.Disabled - if(item.isEnabled !== haveEnabled) { - simulatedNimModel.setProperty(i, "isEnabled", haveEnabled) - } - } - } - - function areAllEnabled(modelToCheck) { - for (let i = 0; i < modelToCheck.count; i++) { - if(!(modelToCheck.get(i).isEnabled)) { - return false - } - } - return true - } - } } // category: Popups diff --git a/storybook/pages/ReceiveModalPage.qml b/storybook/pages/ReceiveModalPage.qml index 8093e00704..9da6d5f568 100644 --- a/storybook/pages/ReceiveModalPage.qml +++ b/storybook/pages/ReceiveModalPage.qml @@ -74,27 +74,16 @@ SplitView { return result } - function processPreferredSharingNetworkToggle(preferredSharingNetworks, toggledNetwork) { - let prefChains = preferredSharingNetworks - if(prefChains.length === filteredFlatModel.count) { - prefChains = [toggledNetwork.chainId.toString()] + function getNetworkIds(chainShortNames) { + let result = "" + if (!chainShortNames) return result + + let shortNames = chainShortNames.split(":").filter((shortName) => shortName.length > 0) + for(let i = 0; i< shortNames.length; i++) { + let chainId = ModelUtils.getByKey(NetworksModel.flatNetworks, "shortName", shortNames[i]).chainId + result += ":" + chainId.toString() } - else if(!prefChains.includes(toggledNetwork.chainId.toString())) { - prefChains.push(toggledNetwork.chainId.toString()) - } - else { - if(prefChains.length === 1) { - prefChains = getAllNetworksChainIds() - } - else { - for(var i = 0; i < prefChains.length;i++) { - if(prefChains[i] === toggledNetwork.chainId.toString()) { - prefChains.splice(i, 1) - } - } - } - } - return prefChains + return result } function addressWasShown(account) { diff --git a/storybook/pages/WalletHeaderPage.qml b/storybook/pages/WalletHeaderPage.qml index 26dcc8ac4d..e0832bf74c 100644 --- a/storybook/pages/WalletHeaderPage.qml +++ b/storybook/pages/WalletHeaderPage.qml @@ -85,6 +85,7 @@ SplitView { filters: ValueFilter { roleName: "isTest"; value: false } } function toggleNetwork(chainId) { + print ("toggleNetwork called with chainId: " + chainId) } function getAllNetworksSupportedString(hovered) { diff --git a/storybook/qmlTests/tests/tst_NetworkSelectPopup.qml b/storybook/qmlTests/tests/tst_NetworkSelectPopup.qml new file mode 100644 index 0000000000..763a2c4da0 --- /dev/null +++ b/storybook/qmlTests/tests/tst_NetworkSelectPopup.qml @@ -0,0 +1,89 @@ +import QtQuick 2.14 +import QtTest 1.15 + +import AppLayouts.Wallet.popups 1.0 + +import utils 1.0 + +import Models 1.0 + +Item { + id: root + width: 600 + height: 400 + + Component { + id: componentUnderTest + NetworkSelectPopup { + anchors.centerIn: parent + flatNetworks: NetworksModel.flatNetworks + visible: true + } + } + + SignalSpy { + id: selectionChangedSpy + target: controlUnderTest + signalName: "onSelectionChanged" + } + + property NetworkSelectPopup controlUnderTest: null + + TestCase { + name: "NetworkSelectPopup" + when: windowShown + + function init() { + controlUnderTest = createTemporaryObject(componentUnderTest, root) + controlUnderTest.open() + compare(controlUnderTest.opened, true) + selectionChangedSpy.clear() + } + + function test_basicGeometry() { + verify(!!controlUnderTest) + compare(controlUnderTest.width, 300) + compare(controlUnderTest.height, controlUnderTest.contentHeight + controlUnderTest.padding * 2) + } + + function test_selectionBindings() { + //single selection - select using the selectio property + compare(controlUnderTest.multiSelection, false) + controlUnderTest.selection = [controlUnderTest.flatNetworks.get(0).chainId] + compare(controlUnderTest.selection, [controlUnderTest.flatNetworks.get(0).chainId]) + compare(selectionChangedSpy.count, 1) + + //single selection - select using the view + const secondDelegate = findChild(controlUnderTest.contentItem, "networkSelectorDelegate_" + controlUnderTest.flatNetworks.get(1).chainName) + mouseClick(secondDelegate) + compare(controlUnderTest.selection, [controlUnderTest.flatNetworks.get(1).chainId]) + compare(selectionChangedSpy.count, 2) + + // multi selection - select using selection property + controlUnderTest.open() + controlUnderTest.multiSelection = true + controlUnderTest.selection = [controlUnderTest.flatNetworks.get(0).chainId, controlUnderTest.flatNetworks.get(1).chainId] + compare(controlUnderTest.selection, [controlUnderTest.flatNetworks.get(0).chainId, controlUnderTest.flatNetworks.get(1).chainId]) + compare(selectionChangedSpy.count, 3) + + // multi selection - select using the view + const thirdDelegate = findChild(controlUnderTest.contentItem, "networkSelectorDelegate_" + controlUnderTest.flatNetworks.get(2).chainName) + mouseClick(thirdDelegate) + compare(controlUnderTest.selection, [controlUnderTest.flatNetworks.get(0).chainId, controlUnderTest.flatNetworks.get(1).chainId, controlUnderTest.flatNetworks.get(2).chainId]) + compare(selectionChangedSpy.count, 4) + } + + function test_closeAfterSingleSelection() { + compare(controlUnderTest.multiSelection, false) + const secondDelegate = findChild(controlUnderTest.contentItem, "networkSelectorDelegate_" + controlUnderTest.flatNetworks.get(1).chainName) + mouseClick(secondDelegate) + compare(controlUnderTest.opened, false) + + controlUnderTest.open() + controlUnderTest.multiSelection = true + const thirdDelegate = findChild(controlUnderTest.contentItem, "networkSelectorDelegate_" + controlUnderTest.flatNetworks.get(2).chainName) + mouseClick(thirdDelegate) + compare(controlUnderTest.opened, true) + } + } +} \ No newline at end of file diff --git a/storybook/qmlTests/tests/tst_NetworkSelectorView.qml b/storybook/qmlTests/tests/tst_NetworkSelectorView.qml index a584e3524c..3ecda1ffef 100644 --- a/storybook/qmlTests/tests/tst_NetworkSelectorView.qml +++ b/storybook/qmlTests/tests/tst_NetworkSelectorView.qml @@ -81,7 +81,7 @@ Item { verify(!!delegate) compare(delegate.title, model.chainName) - compare(delegate.iconUrl, Style.svg(model.iconUrl)) + compare(delegate.iconUrl, (model.isTest ? Style.svg(model.iconUrl + "-test") : Style.svg(model.iconUrl))) compare(delegate.showIndicator, controlUnderTest.showIndicator) compare(delegate.multiSelection, controlUnderTest.multiSelection) compare(delegate.checkState, controlUnderTest.selection.includes(model.chainId) ? Qt.Checked : Qt.Unchecked) diff --git a/storybook/src/Models/NetworksModel.qml b/storybook/src/Models/NetworksModel.qml index 65a7c85e4d..afcdaaea17 100644 --- a/storybook/src/Models/NetworksModel.qml +++ b/storybook/src/Models/NetworksModel.qml @@ -100,7 +100,7 @@ QtObject { chainId: 420, chainName: "Optimism Goerli Testnet", blockExplorerUrl: "https://goerli-optimism.etherscan.io/", - iconUrl: "network/Network=Testnet", + iconUrl: "network/Network=Optimism", chainColor: "#939BA1", shortName: "goOpt", nativeCurrencyName: "Ether", @@ -128,7 +128,7 @@ QtObject { chainId: 421613, chainName: "Arbitrum Goerli", blockExplorerUrl: "https://goerli.arbiscan.io/", - iconUrl: "network/Network=Testnet", + iconUrl: "network/Network=Arbitrum", chainColor: "#939BA1", shortName: "goArb", nativeCurrencyName: "Ether", diff --git a/ui/StatusQ/src/StatusQ/Controls/StatusComboBox.qml b/ui/StatusQ/src/StatusQ/Controls/StatusComboBox.qml index 9147613300..b3783efe1a 100644 --- a/ui/StatusQ/src/StatusQ/Controls/StatusComboBox.qml +++ b/ui/StatusQ/src/StatusQ/Controls/StatusComboBox.qml @@ -17,6 +17,7 @@ Item { property alias contentItem: comboBox.contentItem property alias comboBoxListViewSection: listView.section readonly property alias indicator: statusIndicator + property alias popup: comboBox.popup property alias currentIndex: comboBox.currentIndex property alias currentValue: comboBox.currentValue diff --git a/ui/app/AppLayouts/Communities/views/EditOwnerTokenView.qml b/ui/app/AppLayouts/Communities/views/EditOwnerTokenView.qml index 40c730ccb6..2c5873b7d6 100644 --- a/ui/app/AppLayouts/Communities/views/EditOwnerTokenView.qml +++ b/ui/app/AppLayouts/Communities/views/EditOwnerTokenView.qml @@ -1,6 +1,7 @@ import QtQuick 2.15 import QtQuick.Layouts 1.14 +import StatusQ 0.1 import StatusQ.Components 0.1 import StatusQ.Controls 0.1 import StatusQ.Core 0.1 @@ -80,8 +81,6 @@ StatusScrollView { contentWidth: mainLayout.width contentHeight: mainLayout.height - Component.onCompleted: networkSelector.setChain(ownerToken.chainId) - ColumnLayout { id: mainLayout @@ -262,10 +261,6 @@ StatusScrollView { property string label property string description - function setChain(chainId) { netFilter.setChain(chainId) } - - readonly property alias currentNetworkName: netFilter.currentValue - Layout.fillWidth: true Layout.topMargin: Style.current.padding spacing: 8 @@ -282,6 +277,8 @@ StatusScrollView { Layout.fillWidth: true flatNetworks: root.flatNetworks + selection: !!ownerToken.chainId ? [ownerToken.chainId] : [SQUtils.ModelUtils.getByKey(flatNetworks, "layer", 2).chainId/*first layer 2 network*/] + multiSelection: false control.topPadding: 10 control.background: Rectangle { @@ -290,17 +287,17 @@ StatusScrollView { color: "transparent" border.color: Theme.palette.directColor7 } - - onToggleNetwork: (network) => { + + onToggleNetwork: { // Set Owner Token network properties: - ownerToken.chainId = network.chainId - ownerToken.chainName = network.chainName - ownerToken.chainIcon = network.iconUrl + ownerToken.chainId = singleSelectionItemData.chainId + ownerToken.chainName = singleSelectionItemData.chainName + ownerToken.chainIcon = singleSelectionItemData.iconUrl // Set TMaster Token network properties: - tMasterToken.chainId = network.chainId - tMasterToken.chainName = network.chainName - tMasterToken.chainIcon = network.iconUrl + tMasterToken.chainId = singleSelectionItemData.chainId + tMasterToken.chainName = singleSelectionItemData.chainName + tMasterToken.chainIcon = singleSelectionItemData.iconUrl } } } diff --git a/ui/app/AppLayouts/Profile/stores/WalletStore.qml b/ui/app/AppLayouts/Profile/stores/WalletStore.qml index 488583ec85..cd7a28a868 100644 --- a/ui/app/AppLayouts/Profile/stores/WalletStore.qml +++ b/ui/app/AppLayouts/Profile/stores/WalletStore.qml @@ -162,29 +162,6 @@ QtObject { return networksModuleInst.getNetworkShortNames(chainIds) } - function processPreferredSharingNetworkToggle(preferredSharingNetworks, toggledNetwork) { - let prefChains = preferredSharingNetworks - if(prefChains.length === root.flatNetworks.count) { - prefChains = [toggledNetwork.chainId.toString()] - } - else if(!prefChains.includes(toggledNetwork.chainId.toString())) { - prefChains.push(toggledNetwork.chainId.toString()) - } - else { - if(prefChains.length === 1) { - prefChains = getAllNetworksChainIds() - } - else { - for(var i = 0; i < prefChains.length;i++) { - if(prefChains[i] === toggledNetwork.chainId.toString()) { - prefChains.splice(i, 1) - } - } - } - } - return prefChains - } - function copyToClipboard(textToCopy) { globalUtils.copyToClipboard(textToCopy) } diff --git a/ui/app/AppLayouts/Profile/views/wallet/AccountView.qml b/ui/app/AppLayouts/Profile/views/wallet/AccountView.qml index b9aae728e0..f04c4a0211 100644 --- a/ui/app/AppLayouts/Profile/views/wallet/AccountView.qml +++ b/ui/app/AppLayouts/Profile/views/wallet/AccountView.qml @@ -43,12 +43,8 @@ ColumnLayout { readonly property bool privateKeyAccount: !!root.keyPair? root.keyPair.pairType === Constants.keypair.type.privateKeyImport: false readonly property bool seedImport: !!root.keyPair? root.keyPair.pairType === Constants.keypair.type.seedImport: false readonly property string preferredSharingNetworks: !!root.account? root.account.preferredSharingChainIds: "" - property var preferredSharingNetworksArray: preferredSharingNetworks.split(":").filter(Boolean) + property var preferredSharingNetworksArray: preferredSharingNetworks.split(":").filter(Boolean).map(Number) property string preferredSharingNetworkShortNames: walletStore.getNetworkShortNames(preferredSharingNetworks) - onPreferredSharingNetworksChanged: { - preferredSharingNetworksArray = preferredSharingNetworks.split(":").filter(Boolean) - preferredSharingNetworkShortNames = walletStore.getNetworkShortNames(preferredSharingNetworks) - } } spacing: 0 @@ -257,11 +253,15 @@ ColumnLayout { components: [ NetworkFilter { flatNetworks: root.walletStore.filteredFlatModel - preferredNetworksMode: true - preferredSharingNetworks: d.preferredSharingNetworksArray - onToggleNetwork: (network) => { - d.preferredSharingNetworksArray = root.walletStore.processPreferredSharingNetworkToggle(d.preferredSharingNetworksArray, network) - } + multiSelection: true + selection: d.preferredSharingNetworksArray + + onSelectionChanged: { + if (selection !== d.preferredSharingNetworksArray) { + d.preferredSharingNetworksArray = selection + } + } + control.popup.onClosed: { if (!!root.account) { root.walletStore.updateWalletAccountPreferredChains(root.account.address, d.preferredSharingNetworksArray.join(":")) diff --git a/ui/app/AppLayouts/Wallet/controls/NetworkFilter.qml b/ui/app/AppLayouts/Wallet/controls/NetworkFilter.qml index e9c0955cfa..10ad629e1f 100644 --- a/ui/app/AppLayouts/Wallet/controls/NetworkFilter.qml +++ b/ui/app/AppLayouts/Wallet/controls/NetworkFilter.qml @@ -1,4 +1,5 @@ import QtQuick 2.15 +import QtQml 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 @@ -15,6 +16,7 @@ import StatusQ.Controls 0.1 import SortFilterProxyModel 0.2 import AppLayouts.Wallet.helpers 1.0 +import AppLayouts.Wallet.popups 1.0 import utils 1.0 @@ -24,70 +26,28 @@ StatusComboBox { id: root required property var flatNetworks + readonly property alias singleSelectionItemData: d.singleSelectionItem.item + property bool multiSelection: true - property bool preferredNetworksMode: false - property var preferredSharingNetworks: [] + property bool showSelectionIndicator: true property bool showAllSelectedText: true - property bool showCheckboxes: true - property bool showRadioButtons: true property bool showTitle: true + property bool selectionAllowed: true + property var selection: [] - /// \c network is a network.model.nim entry - /// It is called for every toggled network if \c multiSelection is \c true - /// If \c multiSelection is \c false, it is called only for the selected network when the selection changes - signal toggleNetwork(var network) + signal toggleNetwork(int chainId, int index) - function setChain(chainId) { - if(!multiSelection && !!root.flatNetworks && root.flatNetworks.count > 0) { - d.currentIndex = NetworkModelHelpers.getChainIndexByChainId(root.flatNetworks, chainId) - if(d.currentIndex == -1) - d.currentIndex = NetworkModelHelpers.getChainIndexForFirstLayer2Network(root.flatNetworks) - - // Notify change: - root.toggleNetwork(ModelUtils.get(root.flatNetworks, d.currentIndex)) + onSelectionChanged: { + if (root.selection !== networkSelectorView.selection) { + networkSelectorView.selection = root.selection } } - QtObject { - id: d - - readonly property string selectedChainName: { - root.multiSelection - NetworkModelHelpers.getChainName(root.flatNetworks, d.currentIndex) - } - readonly property string selectedIconUrl: { - root.multiSelection - NetworkModelHelpers.getChainIconUrl(root.flatNetworks, d.currentIndex) - } - readonly property bool allSelected: root.preferredNetworksMode ? root.preferredSharingNetworks.length === root.flatNetworks.count : - enabledFlatNetworks.count === root.flatNetworks.count - readonly property bool noneSelected: enabledFlatNetworks.count === 0 - - // Persist selection between selectPopupLoader reloads - property int currentIndex: 0 - - property SortFilterProxyModel enabledFlatNetworks: SortFilterProxyModel { - sourceModel: root.flatNetworks - filters: [ - ValueFilter { roleName: "isEnabled"; value: true; enabled: !root.preferredNetworksMode }, - FastExpressionFilter { - expression: root.preferredSharingNetworks.includes(chainId.toString()) - expectedRoles: ["chainId"] - enabled: root.preferredNetworksMode - } - ] - } - } - - onMultiSelectionChanged: root.setChain() - control.padding: 12 control.spacing: 0 control.rightPadding: 36 control.topPadding: 7 - control.popup.x: root.width - control.popup.width - control.popup.width: 300 control.popup.horizontalPadding: 4 control.popup.verticalPadding: 4 @@ -101,49 +61,46 @@ StatusComboBox { control.indicator: SQP.StatusComboboxIndicator { x: root.control.mirrored ? root.control.horizontalPadding : root.width - width - root.control.horizontalPadding y: root.control.topPadding + (root.control.availableHeight - height) / 2 + visible: !d.selectionUnavailable } control.contentItem: RowLayout { - spacing: Style.current.padding + spacing: Style.current.halfPadding StatusSmartIdenticon { objectName: "contentItemIcon" Layout.alignment: Qt.AlignVCenter asset.height: 24 asset.width: 24 asset.isImage: !root.multiSelection - asset.name: !root.multiSelection ? Style.svg(d.selectedIconUrl) : "" + asset.name: !root.multiSelection ? Style.svg(d.singleSelectionIconUrl) : "" active: !root.multiSelection visible: active } - StatusBaseText { - objectName: "contentItemText" - Layout.alignment: Qt.AlignVCenter - Layout.fillWidth: true - font.pixelSize: Style.current.additionalTextSize - font.weight: Font.Medium - elide: Text.ElideRight - lineHeight: 24 - lineHeightMode: Text.FixedHeight - verticalAlignment: Text.AlignVCenter - text: root.multiSelection ? (d.noneSelected ? qsTr("Select networks"): d.allSelected && root.showAllSelectedText ? qsTr("All networks") : "") - : (root.showTitle ? d.selectedChainName : "") - color: Theme.palette.baseColor1 - visible: !!text - } Row { id: row spacing: -4 visible: (!d.allSelected || !root.showAllSelectedText) && chainRepeater.count > 0 Repeater { id: chainRepeater - model: root.multiSelection ? d.enabledFlatNetworks: [] + model: SortFilterProxyModel { + sourceModel: root.multiSelection ? root.flatNetworks : null + filters: FastExpressionFilter { + expression: { + root.selection + return root.selection.includes(model.chainId) + } + expectedRoles: ["chainId"] + } + } delegate: StatusRoundedImage { id: delegateItem + required property var model + required property int index + width: 24 height: 24 - image.source: Style.svg(model.iconUrl) + image.source: model.isTest ? Style.svg(model.iconUrl + "-test") : Style.svg(model.iconUrl) z: index + 1 - visible: root.preferredNetworksMode ? root.preferredSharingNetworks.includes(model.chainId.toString()): image.source !== "" image.layer.enabled: index < chainRepeater.count - 1 && row.spacing < 0 image.layer.effect: OpacityMask { @@ -167,31 +124,83 @@ StatusComboBox { } } } + + StatusBaseText { + objectName: "contentItemText" + Layout.alignment: Qt.AlignVCenter + Layout.fillWidth: true + font.pixelSize: Style.current.additionalTextSize + font.weight: Font.Medium + elide: Text.ElideRight + lineHeight: 24 + lineHeightMode: Text.FixedHeight + verticalAlignment: Text.AlignVCenter + text: d.titleText + color: Theme.palette.baseColor1 + visible: !!text + } } - control.popup.contentItem: NetworkSelectionView { + popup: NetworkSelectPopup { + id: networkSelectorView + y: control.height + 4 + x: root.width - width + flatNetworks: root.flatNetworks - preferredSharingNetworks: root.preferredSharingNetworks - preferredNetworksMode: root.preferredNetworksMode - showCheckboxes: root.showCheckboxes - showRadioButtons: root.showRadioButtons + selectionAllowed: root.selectionAllowed + multiSelection: root.multiSelection + showSelectionIndicator: root.showSelectionIndicator + selection: root.selection - implicitWidth: contentWidth - implicitHeight: contentHeight - - singleSelection { - enabled: !root.multiSelection - currentModel: root.flatNetworks - currentIndex: d.currentIndex + onSelectionChanged: { + if (root.selection !== networkSelectorView.selection) { + root.selection = networkSelectorView.selection + } } - useEnabledRole: false + onToggleNetwork: root.toggleNetwork(chainId, index) + } - onToggleNetwork: (network, index) => { - d.currentIndex = index - root.toggleNetwork(network) - if(singleSelection.enabled) - control.popup.close() - } + Connections { + target: control.popup + enabled: !root.multiSelection + function onOpened() { + if (d.selectionUnavailable) + control.popup.close() + } + } + + QtObject { + id: d + readonly property bool allSelected: root.selection.length === root.flatNetworks.count + readonly property bool noneSelected: root.selection.length === 0 + readonly property bool oneSelected: root.selection.length === 1 + readonly property bool selectionUnavailable: root.flatNetworks.count <= 1 && d.oneSelected + + readonly property ModelEntry singleSelectionItem: ModelEntry { + sourceModel: d.oneSelected ? root.flatNetworks : null + key: "chainId" + value: root.selection[0] ?? -1 + } + + readonly property string singleSelectionIconUrl: singleSelectionItem.item.iconUrl ?? "" + readonly property string singleCelectionChainName: singleSelectionItem.item.chainName ?? "" + + readonly property string titleText: { + if (d.oneSelected && root.showTitle) { + return d.singleCelectionChainName + } + + if (root.multiSelection) { + if (d.noneSelected) { + return qsTr("Select networks") + } + if (d.allSelected && root.showAllSelectedText) { + return qsTr("All networks") + } + } + + return "" + } } } diff --git a/ui/app/AppLayouts/Wallet/controls/NetworkSelectItemDelegate.qml b/ui/app/AppLayouts/Wallet/controls/NetworkSelectItemDelegate.qml index ab5992679a..907cc5bf11 100644 --- a/ui/app/AppLayouts/Wallet/controls/NetworkSelectItemDelegate.qml +++ b/ui/app/AppLayouts/Wallet/controls/NetworkSelectItemDelegate.qml @@ -43,7 +43,7 @@ StatusListItem { leftPadding: 16 rightPadding: 16 statusListItemTitleArea.anchors.leftMargin: 12 - highlighted: (d.checkState !== Qt.Unchecked && !showIndicator) + highlighted: d.checkState !== Qt.Unchecked && !showIndicator Binding on bgColor { when: highlighted && !root.sensor.containsMouse diff --git a/ui/app/AppLayouts/Wallet/controls/StatusNetworkSelector.qml b/ui/app/AppLayouts/Wallet/controls/StatusNetworkSelector.qml index 599951dbd9..f64c09a9b8 100644 --- a/ui/app/AppLayouts/Wallet/controls/StatusNetworkSelector.qml +++ b/ui/app/AppLayouts/Wallet/controls/StatusNetworkSelector.qml @@ -193,7 +193,7 @@ Rectangle { asset.height: root.asset.height asset.width: root.asset.width - asset.name: root.useLetterIdenticons ? model.text : Style.svg(model.iconUrl) + asset.name: root.useLetterIdenticons ? model.text : (model.isTest ? Style.svg(model.iconUrl + "-test") : Style.svg(model.iconUrl)) asset.isImage: root.asset.isImage asset.bgColor: root.asset.bgColor asset.isLetterIdenticon: root.useLetterIdenticons diff --git a/ui/app/AppLayouts/Wallet/panels/DAppsWorkflow.qml b/ui/app/AppLayouts/Wallet/panels/DAppsWorkflow.qml index 6dc668d208..712ee41ab3 100644 --- a/ui/app/AppLayouts/Wallet/panels/DAppsWorkflow.qml +++ b/ui/app/AppLayouts/Wallet/panels/DAppsWorkflow.qml @@ -96,7 +96,7 @@ ConnectedDappsButton { flatNetworks: root.wcService.flatNetworks onConnect: { - root.wcService.approvePairSession(sessionProposal, dappChains, selectedAccount) + root.wcService.approvePairSession(sessionProposal, selectedChains, selectedAccount) } onDecline: { diff --git a/ui/app/AppLayouts/Wallet/panels/WalletHeader.qml b/ui/app/AppLayouts/Wallet/panels/WalletHeader.qml index 727c44785f..7e1055db32 100644 --- a/ui/app/AppLayouts/Wallet/panels/WalletHeader.qml +++ b/ui/app/AppLayouts/Wallet/panels/WalletHeader.qml @@ -124,10 +124,29 @@ Item { Layout.alignment: Qt.AlignTop flatNetworks: walletStore.filteredFlatModel + onToggleNetwork: walletStore.toggleNetwork(chainId) - onToggleNetwork: (network) => { - walletStore.toggleNetwork(network.chainId) - } + Binding on selection { + value: chainIdsAggregator.value + } + + FunctionAggregator { + id: chainIdsAggregator + + readonly property SortFilterProxyModel enabledNetworksModel: SortFilterProxyModel{ + sourceModel: walletStore.filteredFlatModel + filters: ValueFilter { + roleName: "isEnabled" + value: true + } + } + + model: enabledNetworksModel + initialValue: [] + roleName: "chainId" + + aggregateFunction: (aggr, value) => [...aggr, value] + } } } diff --git a/ui/app/AppLayouts/Wallet/popups/AddEditSavedAddressPopup.qml b/ui/app/AppLayouts/Wallet/popups/AddEditSavedAddressPopup.qml index 7456ddace3..16a3b73115 100644 --- a/ui/app/AppLayouts/Wallet/popups/AddEditSavedAddressPopup.qml +++ b/ui/app/AppLayouts/Wallet/popups/AddEditSavedAddressPopup.qml @@ -9,8 +9,10 @@ import shared.controls 1.0 import shared.panels 1.0 import shared.stores 1.0 as SharedStores +import StatusQ 0.1 import StatusQ.Core 0.1 import StatusQ.Core.Theme 0.1 +import StatusQ.Core.Utils 0.1 as StatusQUtils import StatusQ.Controls 0.1 import StatusQ.Controls.Validators 0.1 import StatusQ.Popups 0.1 @@ -96,6 +98,14 @@ StatusModal { property string storedChainShortNames: "" property bool chainShortNamesDirty: false + property var networkSelection: [] + + onNetworkSelectionChanged: { + if (d.networkSelection !== networkSelectPopup.selection) { + networkSelectPopup.selection = d.networkSelection + } + } + property bool addressInputValid: d.editMode || addressInput.input.dirty && d.addressInputIsAddress && @@ -183,7 +193,7 @@ StatusModal { d.ens = "" d.address = Constants.zeroAddress d.chainShortNames = "" - flatNetworksModelCopy.setEnabledNetworks([]) + d.networkSelection = [] } d.cardsModel.clear() @@ -458,12 +468,12 @@ StatusModal { d.ens = "" d.address = prefixAndAddress.address d.chainShortNames = prefixAndAddress.prefix - - let prefixArrWithColumn = d.getPrefixArrayWithColumns(prefixAndAddress.prefix) - if (!prefixArrWithColumn) - prefixArrWithColumn = [] - - flatNetworksModelCopy.setEnabledNetworks(prefixArrWithColumn) + + Qt.callLater(()=> { + // Sync chain short names with model. This could result in removing networks from this text + // Call it later to avoid binding loop warnings + d.networkSelection = store.getNetworkIds(d.chainShortNames).split(":").filter(Boolean).map(Number) + }) } } @@ -491,9 +501,10 @@ StatusModal { } function getUnknownPrefixes(prefixes) { + const networksCount = root.flatNetworks.rowCount() let unknownPrefixes = prefixes.filter(e => { - for (let i = 0; i < flatNetworksModelCopy.count; i++) { - if (e == flatNetworksModelCopy.get(i).shortName) + for (let i = 0; i < networksCount; i++) { + if (e == StatusQUtils.ModelUtils.get(root.flatNetworks, i).shortName) return false } return true @@ -595,28 +606,25 @@ StatusModal { defaultItemImageSource: "add" rightButtonVisible: true - property bool modelUpdateBlocked: false - - function blockModelUpdate(value) { - modelUpdateBlocked = value - } - itemsModel: SortFilterProxyModel { - sourceModel: flatNetworksModelCopy - filters: ValueFilter { - roleName: "isEnabled" - value: true + sourceModel: root.flatNetworks + filters: FastExpressionFilter { + readonly property var filteredNetworks: d.networkSelection + expression: { + return filteredNetworks.length > 0 && filteredNetworks.includes(model.chainId) + } + expectedRoles: ["chainId"] } onCountChanged: { - if (!networkSelector.modelUpdateBlocked && d.initialized) { + if (d.initialized) { // Initially source model is empty, filter proxy is also empty, but does // extra work and mistakenly overwrites d.chainShortNames property if (sourceModel.count != 0) { const prefixAndAddress = Utils.splitToChainPrefixAndAddress(addressInput.plainText) const syncedPrefix = addressInput.syncChainPrefixWithModel(prefixAndAddress.prefix, this) - d.chainShortNames = syncedPrefix - addressInput.setPlainText(syncedPrefix + prefixAndAddress.address) + if (addressInput.text !== syncedPrefix + prefixAndAddress.address) + addressInput.setPlainText(syncedPrefix + prefixAndAddress.address) } } } @@ -624,17 +632,18 @@ StatusModal { addButton.highlighted: networkSelectPopup.visible addButton.onClicked: { - networkSelectPopup.openAtPosition(addButton.x, networkSelector.y + addButton.height + Style.current.xlPadding) + networkSelectPopup.openAtPosition(addButton.x, addButton.height + Style.current.xlPadding) } onItemClicked: function (item, index, mouse) { // Append first item if (index === 0 && defaultItem.visible) - networkSelectPopup.openAtPosition(defaultItem.x, networkSelector.y + defaultItem.height + Style.current.xlPadding) + networkSelectPopup.openAtPosition(defaultItem.x, defaultItem.height + Style.current.xlPadding) } onItemRightButtonClicked: function (item, index, mouse) { - item.modelRef.isEnabled = !item.modelRef.isEnabled + let networkSelection = [...d.networkSelection] + d.networkSelection = networkSelection.filter(n => n !== item.modelRef.chainId) d.chainShortNamesDirty = true } @@ -659,32 +668,36 @@ StatusModal { } } ] + + NetworkSelectPopup { + id: networkSelectPopup + + function openAtPosition(x, y) { + networkSelectPopup.x = x + networkSelectPopup.y = y + networkSelectPopup.open() + } + + flatNetworks: root.flatNetworks + selection: d.networkSelection + multiSelection: true + + onSelectionChanged: { + if (d.networkSelection !== networkSelectPopup.selection) { + d.networkSelection = networkSelectPopup.selection + d.chainShortNamesDirty = true + } + } + + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + + modal: true + dim: false + } } } } - NetworkSelectPopup { - id: networkSelectPopup - - flatNetworks: flatNetworksModelCopy - - onToggleNetwork: (network) => { - network.isEnabled = !network.isEnabled - d.chainShortNamesDirty = true - } - - closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside - - function openAtPosition(xPos, yPos) { - x = xPos - y = yPos - open() - } - - modal: true - dim: false - } - rightButtons: [ StatusButton { text: d.editMode? qsTr("Save") : qsTr("Add address") @@ -695,21 +708,4 @@ StatusModal { objectName: "addSavedAddress" } ] - - CloneModel { - id: flatNetworksModelCopy - - sourceModel: root.flatNetworks - roles: ["layer", "chainId", "chainColor", "chainName","shortName", "iconUrl"] - rolesOverride: [{ role: "isEnabled", transform: (modelData) => Boolean(false) }] - - function setEnabledNetworks(prefixArr) { - networkSelector.blockModelUpdate(true) - for (let i = 0; i < count; i++) { - // Add only those chainShortNames to the model, that have column ":" at the end, making it a valid chain prefix - setProperty(i, "isEnabled", prefixArr.includes(get(i).shortName + ":")) - } - networkSelector.blockModelUpdate(false) - } - } } diff --git a/ui/app/AppLayouts/Wallet/popups/NetworkSelectPopup.qml b/ui/app/AppLayouts/Wallet/popups/NetworkSelectPopup.qml index 9cf89ea49e..d8d65cc647 100644 --- a/ui/app/AppLayouts/Wallet/popups/NetworkSelectPopup.qml +++ b/ui/app/AppLayouts/Wallet/popups/NetworkSelectPopup.qml @@ -10,45 +10,31 @@ import SortFilterProxyModel 0.2 import utils 1.0 -import "../stores/NetworkSelectPopup" import "../controls" import "../views" -StatusDialog { +Popup { id: root - property var flatNetworks - property var preferredSharingNetworks: [] - property bool preferredNetworksMode: false + required property var flatNetworks - /// Grouped properties for single selection state. \c singleSelection.enabled is \c false by default - /// \see SingleSelectionInfo - property alias singleSelection: d.singleSelection + property bool showSelectionIndicator: true + property bool selectionAllowed: true + property bool multiSelection: false + property var selection: [] - property bool useEnabledRole: true + signal toggleNetwork(int chainId, int index) - /// \c network is a network.model.nim entry. \c model and \c index for the current selection - /// It is called for every toggled network if \c singleSelection.enabled is \c false - /// If \c singleSelection.enabled is \c true, it is called only for the selected network when the selection changes - /// \see SingleSelectionInfo - signal toggleNetwork(var network, int index) - - QtObject { - id: d - - property SingleSelectionInfo singleSelection: SingleSelectionInfo {} + onSelectionChanged: { + if (root.selection !== scrollView.selection) { + scrollView.selection = root.selection + } } modal: false - standardButtons: Dialog.NoButton - - anchors.centerIn: undefined padding: 4 - width: 360 - implicitHeight: Math.min(432, scrollView.contentHeight + root.padding * 2) - - closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + implicitWidth: 300 background: Rectangle { radius: Style.current.radius @@ -65,20 +51,25 @@ StatusDialog { } } - NetworkSelectionView { + contentItem: NetworkSelectorView { id: scrollView - width: parent.width - height: parent.height - anchors.fill: parent - flatNetworks: root.flatNetworks - preferredNetworksMode: root.preferredNetworksMode - preferredSharingNetworks: root.preferredSharingNetworks - useEnabledRole: root.useEnabledRole - singleSelection: d.singleSelection - onToggleNetwork: (network, index) => { - root.toggleNetwork(network, index) - if(d.singleSelection.enabled) - close() + + model: root.flatNetworks + interactive: root.selectionAllowed + multiSelection: root.multiSelection + showIndicator: root.showSelectionIndicator + selection: root.selection + + onSelectionChanged: { + if (root.selection !== scrollView.selection) { + root.selection = scrollView.selection + } + } + + onToggleNetwork: { + if (!root.multiSelection && root.closePolicy !== Popup.NoAutoClose) + root.close() + root.toggleNetwork(chainId, index) } } } diff --git a/ui/app/AppLayouts/Wallet/popups/ReceiveModal.qml b/ui/app/AppLayouts/Wallet/popups/ReceiveModal.qml index 0edfbc2147..8911b4b17f 100644 --- a/ui/app/AppLayouts/Wallet/popups/ReceiveModal.qml +++ b/ui/app/AppLayouts/Wallet/popups/ReceiveModal.qml @@ -45,7 +45,7 @@ StatusModal { signal updatePreferredChains(string address, string preferredChains) onSelectedAccountChanged: { - d.preferredChainIdsArray = root.selectedAccount.preferredSharingChainIds.split(":").filter(Boolean) + d.preferredChainIdsArray = root.selectedAccount.preferredSharingChainIds.split(":").filter(Boolean).map(Number) } width: 556 @@ -158,7 +158,15 @@ StatusModal { readonly property bool multiChainView: tabBar.currentIndex === 1 readonly property int advanceFooterHeight: 88 - property var preferredChainIdsArray: root.selectedAccount.preferredSharingChainIds.split(":").filter(Boolean) + property var preferredChainIdsArray: [] + Binding on preferredChainIdsArray { + value: root.selectedAccount.preferredSharingChainIds.split(":").filter(Boolean).map(Number) + } + onPreferredChainIdsArrayChanged: { + if (preferredChainIdsArray !== selectPopup.selection) { + selectPopup.selection = preferredChainIdsArray + } + } property var preferredChainIds: d.preferredChainIdsArray.join(":") readonly property string preferredChainShortNames: d.multiChainView? root.getNetworkShortNames(d.preferredChainIds) : "" @@ -274,8 +282,8 @@ StatusModal { enabled: false button.visible: false title: model.shortName - asset.name: Style.svg("tiny/" + model.iconUrl) - visible: d.preferredChainIdsArray.includes(model.chainId.toString()) + asset.name: model.isTest ? Style.svg(model.iconUrl + "-test") : Style.svg(model.iconUrl) + visible: d.preferredChainIdsArray.includes(model.chainId) } } } @@ -302,16 +310,15 @@ StatusModal { margins: -1 // to allow positioning outside the bounds of the dialog flatNetworks: root.store.filteredFlatModel - preferredNetworksMode: true - preferredSharingNetworks: d.preferredChainIdsArray - - useEnabledRole: false + selection: d.preferredChainIdsArray + multiSelection: true closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside - onToggleNetwork: (network, index) => { - d.preferredChainIdsArray = store.processPreferredSharingNetworkToggle(d.preferredChainIdsArray, network) - } + onSelectionChanged: { + if (selection !== d.preferredChainIdsArray) + d.preferredChainIdsArray = selection + } onClosed: { root.updatePreferredChains(root.selectedAccount.address, d.preferredChainIds) diff --git a/ui/app/AppLayouts/Wallet/popups/swap/SwapModal.qml b/ui/app/AppLayouts/Wallet/popups/swap/SwapModal.qml index b6d1b0b3d3..626bfedceb 100644 --- a/ui/app/AppLayouts/Wallet/popups/swap/SwapModal.qml +++ b/ui/app/AppLayouts/Wallet/popups/swap/SwapModal.qml @@ -119,15 +119,14 @@ StatusDialog { objectName: "networkFilter" Layout.alignment: Qt.AlignVCenter multiSelection: false - showRadioButtons: false + showSelectionIndicator: false showTitle: false flatNetworks: root.swapAdaptor.filteredFlatNetworksModel - onToggleNetwork: (network) => { - root.swapInputParamsForm.selectedNetworkChainId = network.chainId - } - Component.onCompleted: { - if(root.swapInputParamsForm.selectedNetworkChainId !== -1) - networkFilter.setChain(root.swapInputParamsForm.selectedNetworkChainId) + selection: [root.swapInputParamsForm.selectedNetworkChainId] + onSelectionChanged: { + if (root.swapInputParamsForm.selectedNetworkChainId !== selection[0]) { + root.swapInputParamsForm.selectedNetworkChainId = selection[0] + } } } @@ -141,7 +140,7 @@ StatusDialog { Connections { target: root.swapInputParamsForm function onSelectedNetworkChainIdChanged() { - networkFilter.setChain(root.swapInputParamsForm.selectedNetworkChainId) + networkFilter.selection = [root.swapInputParamsForm.selectedNetworkChainId] } } } diff --git a/ui/app/AppLayouts/Wallet/stores/NetworkSelectPopup/SingleSelectionInfo.qml b/ui/app/AppLayouts/Wallet/stores/NetworkSelectPopup/SingleSelectionInfo.qml deleted file mode 100644 index 0de6a03acd..0000000000 --- a/ui/app/AppLayouts/Wallet/stores/NetworkSelectPopup/SingleSelectionInfo.qml +++ /dev/null @@ -1,8 +0,0 @@ -import QtQml 2.15 - -/// Inline component was failing on Linux with "Cannot assign to property of unknown type" so we need to use a separate file for it. -QtObject { - property bool enabled: false - property var currentModel - property int currentIndex: 0 -} diff --git a/ui/app/AppLayouts/Wallet/stores/RootStore.qml b/ui/app/AppLayouts/Wallet/stores/RootStore.qml index 275fd2115e..324461694e 100644 --- a/ui/app/AppLayouts/Wallet/stores/RootStore.qml +++ b/ui/app/AppLayouts/Wallet/stores/RootStore.qml @@ -474,29 +474,6 @@ QtObject { } } - function processPreferredSharingNetworkToggle(preferredSharingNetworks, toggledNetwork) { - let prefChains = preferredSharingNetworks - if(prefChains.length === root.filteredFlatModel.count) { - prefChains = [toggledNetwork.chainId.toString()] - } - else if(!prefChains.includes(toggledNetwork.chainId.toString())) { - prefChains.push(toggledNetwork.chainId.toString()) - } - else { - if(prefChains.length === 1) { - prefChains = getAllNetworksChainIds() - } - else { - for(var i = 0; i < prefChains.length;i++) { - if(prefChains[i] === toggledNetwork.chainId.toString()) { - prefChains.splice(i, 1) - } - } - } - } - return prefChains - } - function updateWatchAccountHiddenFromTotalBalance(address, hideFromTotalBalance) { walletSectionAccounts.updateWatchAccountHiddenFromTotalBalance(address, hideFromTotalBalance) } diff --git a/ui/app/AppLayouts/Wallet/views/NetworkSelectorView.qml b/ui/app/AppLayouts/Wallet/views/NetworkSelectorView.qml index 86b3117983..c9a0241ee1 100644 --- a/ui/app/AppLayouts/Wallet/views/NetworkSelectorView.qml +++ b/ui/app/AppLayouts/Wallet/views/NetworkSelectorView.qml @@ -21,6 +21,7 @@ StatusListView { chainId [int] - chain unique identifier iconUrl [string] - SVG icon name. e.g. "network/Network=Ethereum" layer [int] - chain layer. e.g. 1 or 2 + isTest [bool] - true if the chain is a testnet **/ property bool showIndicator: true property bool multiSelection: false @@ -71,7 +72,7 @@ StatusListView { height: 48 width: ListView.view.width title: model.chainName - iconUrl: Style.svg(model.iconUrl) + iconUrl: model.isTest ? Style.svg(model.iconUrl + "-test") : Style.svg(model.iconUrl) showIndicator: root.showIndicator multiSelection: root.multiSelection interactive: root.interactive @@ -100,7 +101,8 @@ StatusListView { required property int section width: parent.width height: active ? 44 : 0 - sourceComponent: section === 2 ? layer2text: null + active: section === 2 + sourceComponent: layer2text Component { id: layer2text diff --git a/ui/imports/assets/icons/network/Network=Arbitrum-test.svg b/ui/imports/assets/icons/network/Network=Arbitrum-test.svg new file mode 100644 index 0000000000..113382dc3a --- /dev/null +++ b/ui/imports/assets/icons/network/Network=Arbitrum-test.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/ui/imports/assets/icons/network/Network=Ethereum-test.svg b/ui/imports/assets/icons/network/Network=Ethereum-test.svg new file mode 100644 index 0000000000..80e559685f --- /dev/null +++ b/ui/imports/assets/icons/network/Network=Ethereum-test.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/ui/imports/assets/icons/network/Network=Optimism-test.svg b/ui/imports/assets/icons/network/Network=Optimism-test.svg new file mode 100644 index 0000000000..9fbfc8ad6e --- /dev/null +++ b/ui/imports/assets/icons/network/Network=Optimism-test.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/ui/imports/shared/popups/walletconnect/ConnectDAppModal.qml b/ui/imports/shared/popups/walletconnect/ConnectDAppModal.qml index 59da3af04c..9f9c68a9ff 100644 --- a/ui/imports/shared/popups/walletconnect/ConnectDAppModal.qml +++ b/ui/imports/shared/popups/walletconnect/ConnectDAppModal.qml @@ -8,6 +8,7 @@ import QtGraphicalEffects 1.15 import StatusQ 0.1 import StatusQ.Core 0.1 +import StatusQ.Core.Utils 0.1 import StatusQ.Popups.Dialog 0.1 import StatusQ.Controls 0.1 import StatusQ.Components 0.1 @@ -31,6 +32,7 @@ StatusDialog { required property var flatNetworks readonly property alias selectedAccount: d.selectedAccount + readonly property alias selectedChains: d.selectedChains readonly property int notConnectedStatus: 0 readonly property int connectionSuccessfulStatus: 1 @@ -206,14 +208,21 @@ StatusDialog { Layout.fillWidth: true } - // TODO: replace with a specialized network selection control NetworkFilter { + id: networkFilter Layout.preferredWidth: accountsDropdown.Layout.preferredWidth flatNetworks: d.filteredChains - showAllSelectedText: false - showCheckboxes: false - enabled: d.connectionStatus === root.notConnectedStatus + showTitle: true + multiSelection: true + selectionAllowed: d.connectionStatus === root.notConnectedStatus && d.allChainIdsAggregator.value.length > 1 + selection: d.selectedChains + + onSelectionChanged: { + if (d.selectedChains !== networkFilter.selection) { + d.selectedChains = networkFilter.selection + } + } } } } @@ -372,6 +381,7 @@ StatusDialog { } property var selectedAccount: ({}) + property var selectedChains: allChainIdsAggregator.value readonly property var filteredChains: LeftJoinModel { leftModel: d.dappChains @@ -380,6 +390,14 @@ StatusDialog { joinRole: "chainId" } + readonly property FunctionAggregator allChainIdsAggregator: FunctionAggregator { + model: d.filteredChains + initialValue: [] + roleName: "chainId" + + aggregateFunction: (aggr, value) => [...aggr, value] + } + readonly property var dappChains: ListModel {} property int connectionStatus: notConnectedStatus