diff --git a/storybook/pages/AssetsViewPage.qml b/storybook/pages/AssetsViewPage.qml index e4bbc01b58..869eeacbfc 100644 --- a/storybook/pages/AssetsViewPage.qml +++ b/storybook/pages/AssetsViewPage.qml @@ -34,6 +34,7 @@ SplitView { SplitView.preferredWidth: 600 SplitView.fillHeight: true assets: assetsModel + filterVisible: ctrlFilterVisible.checked onAssetClicked: logs.logEvent("onAssetClicked", ["token"], [token.symbol, token.communityId]) onSendRequested: logs.logEvent("onSendRequested", ["symbol"], arguments) onReceiveRequested: logs.logEvent("onReceiveRequested", ["symbol"], arguments) @@ -48,6 +49,14 @@ SplitView { SplitView.preferredWidth: 250 logsView.logText: logs.logText + + ColumnLayout { + Switch { + id: ctrlFilterVisible + text: "Filter visible" + checked: true + } + } } } diff --git a/storybook/pages/CollectiblesViewPage.qml b/storybook/pages/CollectiblesViewPage.qml index e0bb2668d4..17b452e834 100644 --- a/storybook/pages/CollectiblesViewPage.qml +++ b/storybook/pages/CollectiblesViewPage.qml @@ -40,6 +40,7 @@ SplitView { SplitView.preferredWidth: 600 SplitView.fillHeight: true collectiblesModel: ctrlEmptyModel.checked ? emptyModel : collectiblesModel + filterVisible: ctrlFilterVisible.checked onCollectibleClicked: logs.logEvent("onCollectibleClicked", ["chainId", "contractAddress", "tokenId", "uid"], arguments) onSendRequested: logs.logEvent("onSendRequested", ["symbol"], arguments) onReceiveRequested: logs.logEvent("onReceiveRequested", ["symbol"], arguments) @@ -56,6 +57,11 @@ SplitView { logsView.logText: logs.logText ColumnLayout { + Switch { + id: ctrlFilterVisible + text: "Filter visible" + checked: true + } Switch { id: ctrlEmptyModel text: "Empty model" @@ -67,3 +73,4 @@ SplitView { // category: Views // https://www.figma.com/file/idUoxN7OIW2Jpp3PMJ1Rl8/%E2%9A%99%EF%B8%8F-Settings-%7C-Desktop?type=design&node-id=19558-95270&mode=design&t=ShZOuMRfiIIl2aR8-0 // https://www.figma.com/file/idUoxN7OIW2Jpp3PMJ1Rl8/%E2%9A%99%EF%B8%8F-Settings-%7C-Desktop?type=design&node-id=19558-96427&mode=design&t=ShZOuMRfiIIl2aR8-0 +// https://www.figma.com/file/FkFClTCYKf83RJWoifWgoX/Wallet-v2?node-id=19087%3A293357&mode=dev diff --git a/storybook/pages/ManageAssetsPanelPage.qml b/storybook/pages/ManageAssetsPanelPage.qml index 5c9b390fac..df6af41d0e 100644 --- a/storybook/pages/ManageAssetsPanelPage.qml +++ b/storybook/pages/ManageAssetsPanelPage.qml @@ -47,6 +47,11 @@ SplitView { text: "Dirty: %1".arg(showcasePanel.dirty ? "true" : "false") } + Label { + Layout.fillWidth: true + text: "Has saved settings: %1".arg(showcasePanel.hasSettings ? "true" : "false") + } + Button { text: "Save" onClicked: showcasePanel.saveSettings() @@ -67,6 +72,7 @@ SplitView { } Button { + enabled: showcasePanel.hasSettings text: "Clear settings" onClicked: showcasePanel.clearSettings() } diff --git a/storybook/pages/ManageCollectiblesPanelPage.qml b/storybook/pages/ManageCollectiblesPanelPage.qml index 2f26504701..41e0b32bb3 100644 --- a/storybook/pages/ManageCollectiblesPanelPage.qml +++ b/storybook/pages/ManageCollectiblesPanelPage.qml @@ -50,6 +50,11 @@ SplitView { text: "Dirty: %1".arg(showcasePanel.dirty ? "true" : "false") } + Label { + Layout.fillWidth: true + text: "Has saved settings: %1".arg(showcasePanel.hasSettings ? "true" : "false") + } + Button { text: "Save" onClicked: showcasePanel.saveSettings() @@ -71,6 +76,7 @@ SplitView { } Button { + enabled: showcasePanel.hasSettings text: "Clear settings" onClicked: showcasePanel.clearSettings() } diff --git a/storybook/src/Models/ManageTokensModel.qml b/storybook/src/Models/ManageTokensModel.qml index 260c5eda77..93f574d398 100644 --- a/storybook/src/Models/ManageTokensModel.qml +++ b/storybook/src/Models/ManageTokensModel.qml @@ -44,11 +44,7 @@ ListModel { symbol: "EUR", displayDecimals: 2 }, - currencyPrice: { - amount: 10.37, - symbol: "EUR", - displayDecimals: 2 - }, + currencyPrice: {}, communityId: "ddls", communityName: "Doodles", communityImage: ModelsData.collectibles.doodles // FIXME backend @@ -65,6 +61,7 @@ ListModel { symbol: "EUR", displayDecimals: 2 }, + currencyPrice: {}, communityId: "sox", communityName: "Socks", communityImage: ModelsData.icons.socks @@ -81,17 +78,22 @@ ListModel { symbol: "EUR", displayDecimals: 2 }, + currencyPrice: { + amount: 0.25, + symbol: "EUR", + displayDecimals: 2 + }, changePct24hour: -2.1, communityId: "", communityName: "", communityImage: "" }, { - name: "Ave Maria", - symbol: "AAVE", + name: "Request", + symbol: "REQ", enabledNetworkBalance: { - amount: 23.3, - symbol: "AAVE", + amount: 0.00005, + symbol: "REQ", displayDecimals: 2 }, enabledNetworkCurrencyBalance: { @@ -99,6 +101,11 @@ ListModel { symbol: "EUR", displayDecimals: 2 }, + currencyPrice: { + amount: 0.1000001, + symbol: "EUR", + displayDecimals: 2 + }, changePct24hour: 4.56, communityId: "", communityName: "", @@ -116,6 +123,11 @@ ListModel { symbol: "EUR", displayDecimals: 2 }, + currencyPrice: { + amount: 0.000752089, + symbol: "EUR", + displayDecimals: 2 + }, changePct24hour: -11.6789, communityId: "", communityName: "", @@ -134,6 +146,11 @@ ListModel { symbol: "EUR", displayDecimals: 2 }, + currencyPrice: { + amount: 0.937718773, + symbol: "EUR", + displayDecimals: 2 + }, changePct24hour: 0, communityId: "", communityName: "", @@ -152,6 +169,7 @@ ListModel { symbol: "EUR", displayDecimals: 2 }, + currencyPrice: {}, changePct24hour: -1, communityId: "", communityName: "", @@ -161,13 +179,13 @@ ListModel { name: "Ethereum", symbol: "ETH", enabledNetworkBalance: { - amount: 0.12345, + amount: 0.123456789, symbol: "ETH", displayDecimals: 8, stripTrailingZeroes: true }, enabledNetworkCurrencyBalance: { - amount: 182.72, + amount: 182.73004849, symbol: "EUR", displayDecimals: 2 }, @@ -186,6 +204,7 @@ ListModel { symbol: "InvisibleSYM", enabledNetworkBalance: {}, enabledNetworkCurrencyBalance: {}, + currencyPrice: {}, changePct24hour: NaN, communityId: "", communityName: "", @@ -193,7 +212,7 @@ ListModel { }, { enabledNetworkBalance: ({ - displayDecimals: true, + displayDecimals: 4, stripTrailingZeroes: true, amount: 0, symbol: "SNT" @@ -201,7 +220,7 @@ ListModel { enabledNetworkCurrencyBalance: ({ displayDecimals: 4, stripTrailingZeroes: true, - amount: 0, + amount: 0., symbol: "EUR" }), currencyPrice: { @@ -228,6 +247,7 @@ ListModel { symbol: "EUR", displayDecimals: 2 }, + currencyPrice: {}, communityId: "ddls", communityName: "Doodles", communityImage: ModelsData.collectibles.doodles @@ -244,6 +264,7 @@ ListModel { symbol: "EUR", displayDecimals: 2 }, + currencyPrice: {}, communityId: "ast", communityName: "Astafarians", communityImage: ModelsData.icons.dribble diff --git a/ui/StatusQ/src/rolesrenamingmodel.cpp b/ui/StatusQ/src/rolesrenamingmodel.cpp index 6b936a9fb0..5024afd626 100644 --- a/ui/StatusQ/src/rolesrenamingmodel.cpp +++ b/ui/StatusQ/src/rolesrenamingmodel.cpp @@ -78,11 +78,9 @@ QQmlListProperty RolesRenamingModel::mapping() QHash RolesRenamingModel::roleNames() const { - QHash roles = sourceModel() - ? sourceModel()->roleNames() - : QHash{}; + const auto roles = sourceModel() ? sourceModel()->roleNames() : QHash{}; - if (roles.empty()) + if (roles.isEmpty()) return roles; QHash renameMap; diff --git a/ui/StatusQ/src/submodelproxymodel.cpp b/ui/StatusQ/src/submodelproxymodel.cpp index 7b61f8e616..3895e15582 100644 --- a/ui/StatusQ/src/submodelproxymodel.cpp +++ b/ui/StatusQ/src/submodelproxymodel.cpp @@ -23,7 +23,7 @@ QVariant SubmodelProxyModel::data(const QModelIndex &index, int role) const ? creationContext : m_delegateModel->engine()->rootContext(); auto context = new QQmlContext(parentContext, parentContext); - context->setContextProperty("submodel", submodel); + context->setContextProperty(QStringLiteral("submodel"), submodel); QObject* instance = m_delegateModel->create(context); QQmlEngine::setObjectOwnership(instance, QQmlEngine::JavaScriptOwnership); diff --git a/ui/StatusQ/src/wallet/managetokenscontroller.cpp b/ui/StatusQ/src/wallet/managetokenscontroller.cpp index 6aa2186390..d69a6a8b57 100644 --- a/ui/StatusQ/src/wallet/managetokenscontroller.cpp +++ b/ui/StatusQ/src/wallet/managetokenscontroller.cpp @@ -164,12 +164,15 @@ void ManageTokensController::clearSettings() m_settings.remove(QString()); m_settings.endGroup(); m_settings.sync(); + + emit settingsDirtyChanged(false); } void ManageTokensController::loadSettings() { Q_ASSERT(!m_settingsKey.isEmpty()); + setSettingsDirty(true); m_settingsData.clear(); // load from QSettings @@ -189,6 +192,7 @@ void ManageTokensController::loadSettings() } m_settings.endArray(); m_settings.endGroup(); + setSettingsDirty(false); } void ManageTokensController::setSettingsDirty(bool dirty) @@ -212,7 +216,7 @@ bool ManageTokensController::hasSettings() const { Q_ASSERT(!m_settingsKey.isEmpty()); const auto groups = m_settings.childGroups(); - return !groups.isEmpty() && groups.contains(settingsGroupName()); + return groups.contains(settingsGroupName()); } void ManageTokensController::settingsHideToken(const QString& symbol) diff --git a/ui/StatusQ/src/wallet/managetokenscontroller.h b/ui/StatusQ/src/wallet/managetokenscontroller.h index 2ad99b1bd3..68b5b54b4e 100644 --- a/ui/StatusQ/src/wallet/managetokenscontroller.h +++ b/ui/StatusQ/src/wallet/managetokenscontroller.h @@ -25,6 +25,7 @@ class ManageTokensController : public QObject, public QQmlParserStatus Q_PROPERTY(QAbstractItemModel* communityTokenGroupsModel READ communityTokenGroupsModel CONSTANT FINAL) Q_PROPERTY(QAbstractItemModel* hiddenTokensModel READ hiddenTokensModel CONSTANT FINAL) Q_PROPERTY(bool dirty READ dirty NOTIFY dirtyChanged FINAL) + Q_PROPERTY(bool hasSettings READ hasSettings NOTIFY settingsDirtyChanged FINAL) Q_PROPERTY(bool settingsDirty READ settingsDirty NOTIFY settingsDirtyChanged FINAL) public: @@ -38,7 +39,6 @@ public: Q_INVOKABLE void saveSettings(bool reuseCurrent = false); Q_INVOKABLE void clearSettings(); Q_INVOKABLE void revert(); - Q_INVOKABLE bool hasSettings() const; Q_INVOKABLE void settingsHideToken(const QString& symbol); Q_INVOKABLE void settingsHideCommunityTokens(const QString& communityId, const QStringList& symbols); @@ -95,6 +95,7 @@ private: void setSettingsKey(const QString& newSettingsKey); QSettings m_settings; SerializedTokenData m_settingsData; // symbol -> {sortOrder, visible, groupId} + bool hasSettings() const; bool m_settingsDirty{false}; bool settingsDirty() const { return m_settingsDirty; } diff --git a/ui/app/AppLayouts/Profile/ProfileLayout.qml b/ui/app/AppLayouts/Profile/ProfileLayout.qml index 3b09c34e29..efa0e46723 100644 --- a/ui/app/AppLayouts/Profile/ProfileLayout.qml +++ b/ui/app/AppLayouts/Profile/ProfileLayout.qml @@ -54,6 +54,7 @@ StatusSectionLayout { keycardView.item.handleBackAction() break; } + Global.settingsSubSubsection = -1 } Component.onCompleted: { diff --git a/ui/app/AppLayouts/Profile/views/WalletView.qml b/ui/app/AppLayouts/Profile/views/WalletView.qml index e235901385..0c7e4a37e0 100644 --- a/ui/app/AppLayouts/Profile/views/WalletView.qml +++ b/ui/app/AppLayouts/Profile/views/WalletView.qml @@ -2,6 +2,7 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.13 import QtGraphicalEffects 1.13 +import QtQml 2.15 import StatusQ.Controls 0.1 import StatusQ.Components 0.1 @@ -88,6 +89,14 @@ SettingsContentBase { accountView.height currentIndex: mainViewIndex + Binding on currentIndex { + value: root.manageTokensViewIndex + when: Global.settingsSubSubsection === Constants.walletSettingsSubsection.manageAssets || + Global.settingsSubSubsection === Constants.walletSettingsSubsection.manageCollectibles || + Global.settingsSubSubsection === Constants.walletSettingsSubsection.manageTokenLists + restoreMode: Binding.RestoreNone + } + onCurrentIndexChanged: { root.rootStore.backButtonName = "" root.sectionTitle = root.walletSectionTitle @@ -257,6 +266,23 @@ SettingsContentBase { // TODO concat proxy model to include community collectibles (#12519) return RootStore.collectiblesStore.ownedCollectibles } + + Binding on currentIndex { + value: { + switch (Global.settingsSubSubsection) { + case Constants.walletSettingsSubsection.manageAssets: + return 0 + case Constants.walletSettingsSubsection.manageCollectibles: + return 1 + case Constants.walletSettingsSubsection.manageTokenLists: + return 2 + } + } + when: Global.settingsSubSubsection === Constants.walletSettingsSubsection.manageAssets || + Global.settingsSubSubsection === Constants.walletSettingsSubsection.manageCollectibles || + Global.settingsSubSubsection === Constants.walletSettingsSubsection.manageTokenLists + restoreMode: Binding.RestoreNone + } } DappPermissionsView { diff --git a/ui/app/AppLayouts/Profile/views/wallet/ManageTokensView.qml b/ui/app/AppLayouts/Profile/views/wallet/ManageTokensView.qml index e3d6dcd9bd..874aa0f8d7 100644 --- a/ui/app/AppLayouts/Profile/views/wallet/ManageTokensView.qml +++ b/ui/app/AppLayouts/Profile/views/wallet/ManageTokensView.qml @@ -18,6 +18,8 @@ ColumnLayout { required property var baseWalletAssetsModel required property var baseWalletCollectiblesModel + property alias currentIndex: tabBar.currentIndex + readonly property bool dirty: { if (!loader.item) return false diff --git a/ui/app/AppLayouts/Wallet/WalletLayout.qml b/ui/app/AppLayouts/Wallet/WalletLayout.qml index ed69313ee9..3d4b140c0b 100644 --- a/ui/app/AppLayouts/Wallet/WalletLayout.qml +++ b/ui/app/AppLayouts/Wallet/WalletLayout.qml @@ -197,6 +197,7 @@ Item { Component { id: receiveModalComponent ReceiveModal { + destroyOnClose: true anchors.centerIn: parent } } diff --git a/ui/app/AppLayouts/Wallet/controls/SortOrderComboBox.qml b/ui/app/AppLayouts/Wallet/controls/SortOrderComboBox.qml index 9551d99f4b..383f9d1f02 100644 --- a/ui/app/AppLayouts/Wallet/controls/SortOrderComboBox.qml +++ b/ui/app/AppLayouts/Wallet/controls/SortOrderComboBox.qml @@ -13,79 +13,71 @@ import utils 1.0 ComboBox { id: root - property int sortOrder: Qt.DescendingOrder - readonly property string currentSortRoleName: d.currentSortRoleName + // expected model role names: text, value (enum TokenOrder), sortRoleName, icon (optional) + // text === "---" denotes a separator + + property bool hasCustomOrderDefined + + property int currentSortOrder: Qt.DescendingOrder + readonly property string currentSortRoleName: root.currentIndex !== -1 ? root.model[root.currentIndex].sortRoleName : "" + + signal createOrEditRequested() - model: d.predefinedSortModel textRole: "text" valueRole: "value" - displayText: !d.isCustomSortOrder ? "%1 %2".arg(currentText).arg(sortOrder === Qt.DescendingOrder ? "↓" : "↑") - : currentText - Component.onCompleted: currentIndex = indexOfValue(SortOrderComboBox.TokenOrderCustom) + displayText: root.currentValue === SortOrderComboBox.TokenOrderCustom ? currentText + : "%1 %2".arg(currentText).arg(currentSortOrder === Qt.DescendingOrder ? "↓" : "↑") + + onActivated: { + if (index === indexOfValue(SortOrderComboBox.TokenOrderCreateCustom)) { // restore the previous sort role and signal we want create/edit + currentIndex = d.currentIndex + root.createOrEditRequested() + } else { + if (d.currentIndex === index) // just keep the same sort role and flip the up/down + currentSortOrder = currentSortOrder === Qt.AscendingOrder ? Qt.DescendingOrder : Qt.AscendingOrder + + // update internal index + d.currentIndex = index + } + } + + Component.onCompleted: { + d.currentIndex = root.currentIndex // sync with settings which might arrive from the outside + } enum TokenOrder { TokenOrderNone = 0, - TokenOrderCustom, - TokenOrderValue, - TokenOrderBalance, - TokenOrder1WChange, - TokenOrderAlpha + TokenOrderCurrencyBalance, // FIAT value of asset balance (enabledNetworkCurrencyBalance) + TokenOrderBalance, // Number of tokens (enabledNetworkBalance) + TokenOrderCurrencyPrice, // Value per token in FIAT (currencyPrice) + TokenOrder1WChange, // Level of change in asset balance value (in FIAT) comp. to 7 days earlier + TokenOrderAlpha, // Alphabetic by asset name (name) + TokenOrderDateAdded, // Date added descending (newest first) + TokenOrderGroupName, // Collection or Community name + TokenOrderCustom, // Custom (user created) order + TokenOrderCreateCustom // special menu entry to create/edit the custom sort order } horizontalPadding: 12 - verticalPadding: 8 - spacing: 8 + verticalPadding: Style.current.halfPadding + spacing: Style.current.halfPadding font.family: Theme.palette.baseFont.name font.pixelSize: Style.current.additionalTextSize - QtObject { id: d readonly property int defaultDelegateHeight: 34 -// // models -// readonly property SortFilterProxyModel tokensModel: SortFilterProxyModel { -// sourceModel: root.baseModel -// proxyRoles: [ -// ExpressionRole { -// name: "currentBalance" -// expression: model.enabledNetworkBalance.amount -// }, -// ExpressionRole { -// name: "currentCurrencyBalance" -// expression: model.enabledNetworkCurrencyBalance.amount -// } -// ] -// sorters: RoleSorter { -// roleName: cmbTokenOrder.currentSortRoleName -// sortOrder: cmbTokenOrder.sortOrder -// enabled: !d.isCustomSortOrder -// } -// filters: ValueFilter { -// roleName: "visibleForNetworkWithPositiveBalance" -// value: true -// } -// } - - readonly property var predefinedSortModel: [ - { value: SortOrderComboBox.TokenOrderValue, text: qsTr("Token value"), icon: "token-sale", sortRoleName: "currentCurrencyBalance" }, // custom SFPM ExpressionRole - { value: SortOrderComboBox.TokenOrderBalance, text: qsTr("Token balance"), icon: "wallet", sortRoleName: "currentBalance" }, // custom SFPM ExpressionRole - { value: SortOrderComboBox.TokenOrder1WChange, text: qsTr("1W change"), icon: "time", sortRoleName: "changePct24hour" }, // FIXME changePct1Week role missing in backend!!! - { value: SortOrderComboBox.TokenOrderAlpha, text: qsTr("Alphabetic"), icon: "bold", sortRoleName: "name" }, - { value: SortOrderComboBox.TokenOrderNone, text: "---", icon: "", sortRoleName: "" }, - { value: SortOrderComboBox.TokenOrderCustom, text: qsTr("Custom order"), icon: "exchange", sortRoleName: "" } - ] - readonly property string currentSortRoleName: root.currentIndex !== -1 ? d.predefinedSortModel[root.currentIndex].sortRoleName : "" - readonly property bool isCustomSortOrder: root.currentValue === SortOrderComboBox.TokenOrderCustom + property int currentIndex: 0 } background: Rectangle { border.width: 1 border.color: Theme.palette.directColor7 - radius: 8 + radius: Style.current.radius color: root.down ? Theme.palette.baseColor2 : "transparent" HoverHandler { cursorShape: root.enabled ? Qt.PointingHandCursor : undefined @@ -116,16 +108,15 @@ ComboBox { closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent y: root.height + 4 - implicitWidth: root.width - margins: 8 + implicitWidth: 290 + margins: Style.current.halfPadding padding: 1 - verticalPadding: 8 + verticalPadding: Style.current.halfPadding background: Rectangle { color: Theme.palette.statusSelect.menuItemBackgroundColor - radius: 8 - border.color: Theme.palette.baseColor2 + radius: Style.current.radius layer.enabled: true layer.effect: DropShadow { horizontalOffset: 0 @@ -138,6 +129,7 @@ ComboBox { } contentItem: ColumnLayout { + spacing: 0 StatusBaseText { Layout.fillWidth: true Layout.preferredHeight: d.defaultDelegateHeight @@ -164,20 +156,18 @@ ComboBox { spacing: root.spacing StatusIcon { + Layout.preferredWidth: 16 + Layout.preferredHeight: 16 visible: !!icon icon: iconName color: root.enabled ? Theme.palette.primaryColor1 : Theme.palette.baseColor1 - width: 16 - height: 16 } StatusBaseText { Layout.fillWidth: true - Layout.fillHeight: true text: menuText - verticalAlignment: Text.AlignVCenter elide: Text.ElideRight - color: root.enabled ? Theme.palette.directColor1 : Theme.palette.baseColor1 + color: isEditAction ? Theme.palette.primaryColor1 : root.enabled ? Theme.palette.directColor1 : Theme.palette.baseColor1 font.pixelSize: root.font.pixelSize font.weight: root.currentIndex === menuIndex ? Font.DemiBold : Font.Normal } @@ -185,7 +175,7 @@ ComboBox { Item { Layout.fillWidth: true } Row { - visible: !isCustomOrder + visible: showUpDownArrows spacing: 4 StatusFlatRoundButton { radius: 6 @@ -195,11 +185,12 @@ ComboBox { icon.width: 18 icon.height: 18 opacity: root.highlightedIndex === menuIndex || highlighted // not "visible, we want the item to stay put - highlighted: root.currentIndex === menuIndex && root.sortOrder === Qt.AscendingOrder + highlighted: root.currentIndex === menuIndex && root.currentSortOrder === Qt.AscendingOrder onClicked: { if (root.currentIndex !== menuIndex) root.currentIndex = menuIndex - root.sortOrder = Qt.AscendingOrder + d.currentIndex = menuIndex + root.currentSortOrder = Qt.AscendingOrder root.popup.close() } } @@ -211,11 +202,12 @@ ComboBox { icon.width: 18 icon.height: 18 opacity: root.highlightedIndex === menuIndex || highlighted // not "visible, we want the item to stay put - highlighted: root.currentIndex === menuIndex && root.sortOrder === Qt.DescendingOrder + highlighted: root.currentIndex === menuIndex && root.currentSortOrder === Qt.DescendingOrder onClicked: { if (root.currentIndex !== menuIndex) root.currentIndex = menuIndex - root.sortOrder = Qt.DescendingOrder + d.currentIndex = menuIndex + root.currentSortOrder = Qt.DescendingOrder root.popup.close() } } @@ -234,9 +226,15 @@ ComboBox { readonly property bool isSeparator: text === "---" id: menuDelegate - width: root.width + width: ListView.view.width highlighted: root.highlightedIndex === index enabled: !isSeparator + visible: { + if (modelData["value"] === SortOrderComboBox.TokenOrderCustom) // hide "Custom order" menu entry if none defined + return root.hasCustomOrderDefined + return true + } + height: visible ? implicitHeight : 0 leftPadding: isSeparator ? 0 : 14 rightPadding: isSeparator ? 0 : 8 verticalPadding: isSeparator ? 2 : 5 @@ -264,9 +262,9 @@ ComboBox { readonly property int menuIndex: menuDelegate.index readonly property string menuText: menuDelegate.text readonly property string iconName: menuDelegate.icon.name - readonly property bool isCustomOrder: !menuDelegate.modelData["sortRoleName"] + readonly property bool showUpDownArrows: menuDelegate.modelData["sortRoleName"] !== "" + readonly property bool isEditAction: modelData["value"] === SortOrderComboBox.TokenOrderCreateCustom sourceComponent: menuDelegate.isSeparator ? separatorMenuComponent : regularMenuComponent } - onClicked: root.currentIndex = index } } diff --git a/ui/app/AppLayouts/Wallet/panels/ManageAssetsPanel.qml b/ui/app/AppLayouts/Wallet/panels/ManageAssetsPanel.qml index 4715feca2b..b981fc8878 100644 --- a/ui/app/AppLayouts/Wallet/panels/ManageAssetsPanel.qml +++ b/ui/app/AppLayouts/Wallet/panels/ManageAssetsPanel.qml @@ -19,6 +19,7 @@ Control { required property var baseModel readonly property bool dirty: d.controller.dirty + readonly property bool hasSettings: d.controller.hasSettings background: null diff --git a/ui/app/AppLayouts/Wallet/panels/ManageCollectiblesPanel.qml b/ui/app/AppLayouts/Wallet/panels/ManageCollectiblesPanel.qml index fabe649fdf..24a4680214 100644 --- a/ui/app/AppLayouts/Wallet/panels/ManageCollectiblesPanel.qml +++ b/ui/app/AppLayouts/Wallet/panels/ManageCollectiblesPanel.qml @@ -21,6 +21,7 @@ Control { required property var baseModel readonly property bool dirty: d.controller.dirty + readonly property bool hasSettings: d.controller.hasSettings background: null diff --git a/ui/app/AppLayouts/Wallet/views/AssetsDetailView.qml b/ui/app/AppLayouts/Wallet/views/AssetsDetailView.qml index 79fb654981..5293968c5b 100644 --- a/ui/app/AppLayouts/Wallet/views/AssetsDetailView.qml +++ b/ui/app/AppLayouts/Wallet/views/AssetsDetailView.qml @@ -29,6 +29,7 @@ Item { QtObject { id: d property var marketValueStore : RootStore.marketValueStore + readonly property string symbol: root.token ? root.token.symbol : "" } Connections { @@ -286,8 +287,8 @@ Item { } Connections { - target: token - function onSymbolChanged() { graphDetail.updateBalanceStore() } + target: d + function onSymbolChanged() { if (d.symbol) graphDetail.updateBalanceStore() } } } } diff --git a/ui/app/AppLayouts/Wallet/views/CollectiblesView.qml b/ui/app/AppLayouts/Wallet/views/CollectiblesView.qml index 02f546968a..202f22e1f6 100644 --- a/ui/app/AppLayouts/Wallet/views/CollectiblesView.qml +++ b/ui/app/AppLayouts/Wallet/views/CollectiblesView.qml @@ -1,6 +1,7 @@ import QtQuick 2.15 import QtQuick.Layouts 1.15 import QtQuick.Controls 2.15 +import Qt.labs.settings 1.1 import StatusQ 0.1 import StatusQ.Core 0.1 @@ -19,14 +20,16 @@ import shared.popups 1.0 import utils 1.0 import AppLayouts.Wallet.views.collectibles 1.0 +import AppLayouts.Wallet.controls 1.0 import SortFilterProxyModel 0.2 -StatusScrollView { +ColumnLayout { id: root required property var collectiblesModel property bool sendEnabled: true + property bool filterVisible signal collectibleClicked(int chainId, string contractAddress, string tokenId, string uid) signal sendRequested(string symbol) @@ -34,6 +37,8 @@ StatusScrollView { signal switchToCommunityRequested(string communityId) signal manageTokensRequested() + spacing: 0 + QtObject { id: d @@ -41,11 +46,7 @@ StatusScrollView { readonly property int communityCellHeight: 242 readonly property int cellWidth: 176 - readonly property bool isCustomView: d.controller.hasSettings // TODO add respect other predefined orders (#12517) - - function symbolIsVisible(symbol) { - return d.controller.filterAcceptsSymbol(symbol) - } + readonly property bool isCustomView: cmbTokenOrder.currentValue === SortOrderComboBox.TokenOrderCustom readonly property var renamedModel: RolesRenamingModel { sourceModel: root.collectiblesModel @@ -58,121 +59,162 @@ StatusScrollView { ] } - readonly property var regularCollectiblesModel: SortFilterProxyModel { - sourceModel: d.renamedModel - - filters: [ - ExpressionFilter { - expression: { - d.controller.settingsDirty - return d.symbolIsVisible(model.symbol) && !model.communityId - } - } - // TODO add other sort/filter using ManageTokensController (#12517) - ] - sorters: [ - RoleSorter { - roleName: "name" - enabled: !d.isCustomView - }, - ExpressionSorter { - expression: { - d.controller.settingsDirty - return d.controller.lessThan(modelLeft.symbol, modelRight.symbol) - } - enabled: d.isCustomView - } - ] - } - - readonly property var communityCollectiblesModel: SortFilterProxyModel { - sourceModel: d.renamedModel - filters: [ - ExpressionFilter { - expression: { - d.controller.settingsDirty - return d.symbolIsVisible(model.symbol) && !!model.communityId - } - } - // TODO add other sort/filter using ManageTokensController (#12517) - ] - sorters: [ - RoleSorter { - roleName: "name" - enabled: !d.isCustomView - }, - ExpressionSorter { - expression: { - d.controller.settingsDirty - return d.controller.lessThan(modelLeft.symbol, modelRight.symbol) - } - enabled: d.isCustomView - } - ] - } - - readonly property bool hasCollectibles: d.regularCollectiblesModel.count - readonly property bool hasCommunityCollectibles: d.communityCollectiblesModel.count + readonly property bool hasCollectibles: regularCollectiblesView.count + readonly property bool hasCommunityCollectibles: communityCollectiblesView.count readonly property var controller: ManageTokensController { settingsKey: "WalletCollectibles" } function hideAllCommunityTokens(communityId) { - const tokenSymbols = ModelUtils.getAll(communityCollectiblesModel, "symbol", "communityId", communityId) + const tokenSymbols = ModelUtils.getAll(communityCollectiblesView.model, "symbol", "communityId", communityId) d.controller.settingsHideCommunityTokens(communityId, tokenSymbols) } } + component CustomSFPM: SortFilterProxyModel { + id: customFilter + property bool isCommunity + + sourceModel: d.renamedModel + proxyRoles: JoinRole { + name: "groupName" + roleNames: ["collectionName", "communityName"] + } + filters: [ + ExpressionFilter { + expression: { + d.controller.settingsDirty + return d.controller.filterAcceptsSymbol(model.symbol) && (customFilter.isCommunity ? !!model.communityId : !model.communityId) + } + } + ] + sorters: [ + ExpressionSorter { + expression: { + d.controller.settingsDirty + return d.controller.lessThan(modelLeft.symbol, modelRight.symbol) + } + enabled: d.isCustomView + }, + RoleSorter { + roleName: cmbTokenOrder.currentSortRoleName + sortOrder: cmbTokenOrder.currentSortOrder + enabled: !d.isCustomView + } + ] + } + + Settings { + category: "CollectiblesViewSortSettings" + property alias currentSortField: cmbTokenOrder.currentIndex + property alias currentSortOrder: cmbTokenOrder.currentSortOrder + } + ColumnLayout { - width: root.availableWidth - spacing: 0 + Layout.fillWidth: true + Layout.preferredHeight: root.filterVisible && (d.hasCollectibles || d.hasCommunityCollectibles) ? implicitHeight : 0 + spacing: 20 + opacity: Layout.preferredHeight < implicitHeight ? 0 : 1 - ShapeRectangle { - visible: !d.hasCollectibles && !d.hasCommunityCollectibles - Layout.fillWidth: true - text: qsTr("Collectibles will appear here") - } - - CustomGridView { - cellHeight: d.cellHeight - model: d.regularCollectiblesModel - visible: d.hasCollectibles - } + Behavior on Layout.preferredHeight { NumberAnimation { duration: 200; easing.type: Easing.InOutQuad } } + Behavior on opacity { NumberAnimation { duration: 200; easing.type: Easing.InOutQuad } } StatusDialogDivider { Layout.fillWidth: true - Layout.topMargin: Style.current.padding - Layout.bottomMargin: Style.current.halfPadding - visible: d.hasCommunityCollectibles } RowLayout { Layout.fillWidth: true - Layout.leftMargin: Style.current.padding - Layout.rightMargin: Style.current.smallPadding - Layout.bottomMargin: 4 - visible: d.hasCommunityCollectibles + spacing: Style.current.halfPadding + StatusBaseText { - text: qsTr("Community collectibles") color: Theme.palette.baseColor1 + font.pixelSize: Style.current.additionalTextSize + text: qsTr("Sort by:") } - Item { Layout.fillWidth: true } - StatusFlatButton { - Layout.preferredWidth: 32 - Layout.preferredHeight: 32 - icon.name: "info" - textColor: Theme.palette.baseColor1 - horizontalPadding: 0 - verticalPadding: 0 - onClicked: Global.openPopup(communityInfoPopupCmp) + + SortOrderComboBox { + id: cmbTokenOrder + hasCustomOrderDefined: d.controller.hasSettings + model: [ + { value: SortOrderComboBox.TokenOrderDateAdded, text: qsTr("Date added"), icon: "calendar", sortRoleName: "dateAdded" }, // FIXME sortRoleName #12942 + { value: SortOrderComboBox.TokenOrderAlpha, text: qsTr("Collectible name"), icon: "bold", sortRoleName: "name" }, + { value: SortOrderComboBox.TokenOrderGroupName, text: qsTr("Collection/community name"), icon: "group", sortRoleName: "groupName" }, // Custom SFPM role communityName || collectionName + { value: SortOrderComboBox.TokenOrderCustom, text: qsTr("Custom order"), icon: "exchange", sortRoleName: "" }, + { value: SortOrderComboBox.TokenOrderNone, text: "---", icon: "", sortRoleName: "" }, // separator + { value: SortOrderComboBox.TokenOrderCreateCustom, text: hasCustomOrderDefined ? qsTr("Edit custom order →") : qsTr("Create custom order →"), + icon: "", sortRoleName: "" } + ] + onCreateOrEditRequested: { + root.manageTokensRequested() + } } } - CustomGridView { - cellHeight: d.communityCellHeight - model: d.communityCollectiblesModel - visible: d.hasCommunityCollectibles + StatusDialogDivider { + Layout.fillWidth: true + } + } + + ShapeRectangle { + Layout.fillWidth: true + visible: !d.hasCollectibles && !d.hasCommunityCollectibles + text: qsTr("Collectibles will appear here") + } + + StatusScrollView { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.topMargin: Style.current.padding + leftPadding: 0 + verticalPadding: 0 + contentWidth: availableWidth + + ColumnLayout { + width: parent.width + spacing: 0 + + CustomGridView { + id: regularCollectiblesView + cellHeight: d.cellHeight + model: CustomSFPM {} + } + + StatusDialogDivider { + Layout.fillWidth: true + Layout.topMargin: Style.current.padding + Layout.bottomMargin: Style.current.halfPadding + visible: d.hasCommunityCollectibles + } + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: Style.current.padding + Layout.rightMargin: Style.current.smallPadding + Layout.bottomMargin: 4 + visible: d.hasCommunityCollectibles + StatusBaseText { + text: qsTr("Community collectibles") + color: Theme.palette.baseColor1 + } + Item { Layout.fillWidth: true } + StatusFlatButton { + Layout.preferredWidth: 32 + Layout.preferredHeight: 32 + icon.name: "info" + textColor: Theme.palette.baseColor1 + horizontalPadding: 0 + verticalPadding: 0 + onClicked: Global.openPopup(communityInfoPopupCmp) + } + } + + CustomGridView { + id: communityCollectiblesView + cellHeight: d.communityCellHeight + model: CustomSFPM { isCommunity: true } + } } } @@ -224,7 +266,7 @@ StatusScrollView { width: d.cellWidth height: isCommunityCollectible ? d.communityCellHeight : d.cellHeight title: model.name ? model.name : "..." - subTitle: model.collectionName ?? "" + subTitle: model.collectionName ? model.collectionName : model.collectionUid ? model.collectionUid : "" mediaUrl: model.mediaUrl ?? "" mediaType: model.mediaType ?? "" fallbackImageUrl: model.imageUrl ?? "" @@ -330,7 +372,7 @@ StatusScrollView { close() Global.displayToastMessage( qsTr("%1 was successfully hidden. You can toggle collectible visibility via %2.").arg(formattedName) - .arg(`` + qsTr("Settings", "Go to Settings") + ""), + .arg(`` + qsTr("Settings", "Go to Settings") + ""), "", "checkmark-circle", false, @@ -362,7 +404,7 @@ StatusScrollView { close() Global.displayToastMessage( qsTr("%1 community collectibles were successfully hidden. You can toggle collectible visibility via %2.").arg(communityName) - .arg(`` + qsTr("Settings", "Go to Settings") + ""), + .arg(`` + qsTr("Settings", "Go to Settings") + ""), "", "checkmark-circle", false, diff --git a/ui/app/AppLayouts/Wallet/views/RightTabView.qml b/ui/app/AppLayouts/Wallet/views/RightTabView.qml index ac2f47db7c..db0d30b66a 100644 --- a/ui/app/AppLayouts/Wallet/views/RightTabView.qml +++ b/ui/app/AppLayouts/Wallet/views/RightTabView.qml @@ -1,8 +1,9 @@ -import QtQuick 2.13 -import QtQuick.Layouts 1.13 +import QtQuick 2.15 +import QtQuick.Layouts 1.15 import StatusQ.Core 0.1 import StatusQ.Controls 0.1 +import StatusQ.Core.Theme 0.1 import utils 1.0 import shared.controls 1.0 @@ -92,36 +93,46 @@ Item { } } - StatusTabBar { - id: walletTabBar - objectName: "rightSideWalletTabBar" + RowLayout { Layout.fillWidth: true - Layout.topMargin: Style.current.padding + StatusTabBar { + id: walletTabBar + objectName: "rightSideWalletTabBar" + Layout.fillWidth: true + Layout.topMargin: Style.current.padding - StatusTabButton { - leftPadding: 0 - width: implicitWidth - text: qsTr("Assets") + StatusTabButton { + leftPadding: 0 + width: implicitWidth + text: qsTr("Assets") + } + StatusTabButton { + width: implicitWidth + text: qsTr("Collectibles") + } + StatusTabButton { + rightPadding: 0 + width: implicitWidth + text: qsTr("Activity") + } + onCurrentIndexChanged: { + RootStore.setCurrentViewedHoldingType(walletTabBar.currentIndex === 1 ? Constants.TokenType.ERC721 : Constants.TokenType.ERC20) + } } - StatusTabButton { - width: implicitWidth - text: qsTr("Collectibles") - } - StatusTabButton { - rightPadding: 0 - width: implicitWidth - text: qsTr("Activity") - } - onCurrentIndexChanged: { - RootStore.setCurrentViewedHoldingType(walletTabBar.currentIndex === 1 ? Constants.TokenType.ERC721 : Constants.TokenType.ERC20) + StatusFlatButton { + Layout.alignment: Qt.AlignTop + id: filterButton + icon.name: "filter" + checkable: true + icon.color: checked ? Theme.palette.primaryColor1 : Theme.palette.baseColor1 + Behavior on icon.color { ColorAnimation { duration: 200; easing.type: Easing.InOutQuad } } + highlighted: checked } } Loader { id: mainViewLoader Layout.fillWidth: true Layout.fillHeight: true - Layout.topMargin: Style.current.padding - Layout.bottomMargin: Style.current.padding sourceComponent: { switch (walletTabBar.currentIndex) { case 0: return assetsView @@ -138,6 +149,7 @@ Item { overview: RootStore.overview networkConnectionStore: root.networkConnectionStore assetDetailsLaunched: stack.currentIndex === 2 + filterVisible: filterButton.checked onAssetClicked: { assetDetailView.token = token RootStore.setCurrentViewedHolding(token.symbol, Constants.TokenType.ERC20) @@ -152,7 +164,8 @@ Item { } onReceiveRequested: (symbol) => root.launchShareAddressModal() onSwitchToCommunityRequested: (communityId) => Global.switchToCommunity(communityId) - onManageTokensRequested: Global.changeAppSectionBySectionType(Constants.appSection.profile, Constants.settingsSubsection.wallet) + onManageTokensRequested: Global.changeAppSectionBySectionType(Constants.appSection.profile, Constants.settingsSubsection.wallet, + Constants.walletSettingsSubsection.manageAssets) } } Component { @@ -160,6 +173,7 @@ Item { CollectiblesView { collectiblesModel: RootStore.collectiblesStore.ownedCollectibles sendEnabled: root.networkConnectionStore.sendBuyBridgeEnabled && !RootStore.overview.isWatchOnlyAccount && RootStore.overview.canSend + filterVisible: filterButton.checked onCollectibleClicked: { RootStore.collectiblesStore.getDetailedCollectible(chainId, contractAddress, tokenId) RootStore.setCurrentViewedHolding(uid, Constants.TokenType.ERC721) @@ -174,7 +188,8 @@ Item { } onReceiveRequested: (symbol) => root.launchShareAddressModal() onSwitchToCommunityRequested: (communityId) => Global.switchToCommunity(communityId) - onManageTokensRequested: Global.changeAppSectionBySectionType(Constants.appSection.profile, Constants.settingsSubsection.wallet) + onManageTokensRequested: Global.changeAppSectionBySectionType(Constants.appSection.profile, Constants.settingsSubsection.wallet, + Constants.walletSettingsSubsection.manageCollectibles) } } Component { diff --git a/ui/app/mainui/AppMain.qml b/ui/app/mainui/AppMain.qml index ff86e24176..eae69ca235 100644 --- a/ui/app/mainui/AppMain.qml +++ b/ui/app/mainui/AppMain.qml @@ -115,6 +115,7 @@ Item { function onActiveSectionChanged() { createChatView.opened = false + Global.settingsSubSubsection = -1 } function onOpenActivityCenter() { @@ -325,13 +326,14 @@ Item { appMain.rootStore.mainModuleInst.setNthEnabledSectionActive(nthSection) } - function onAppSectionBySectionTypeChanged(sectionType: int, subsection: int) { + function onAppSectionBySectionTypeChanged(sectionType, subsection, settingsSubsection = -1) { if(!appMain.rootStore.mainModuleInst) return appMain.rootStore.mainModuleInst.setActiveSectionBySectionType(sectionType) if (sectionType === Constants.appSection.profile) { Global.settingsSubsection = subsection; + Global.settingsSubSubsection = settingsSubsection; } } @@ -750,7 +752,7 @@ Item { active: appMain.rootStore.profileSectionStore.walletStore.areTestNetworksEnabled delay: false onClicked: Global.openTestnetPopup() - onCloseClicked: hide() + closeBtnVisible: false } ModuleWarning { @@ -1410,7 +1412,11 @@ Item { id: sendModal active: false - function open() { + function open(address = "") { + if (!!address) { + preSelectedRecipient = address + preSelectedRecipientType = TabAddressSelectorView.Type.Address + } this.active = true this.item.open() } @@ -1626,7 +1632,8 @@ Item { const sectionArgs = link.substring(1).split("/") const section = sectionArgs[0] let subsection = sectionArgs.length > 1 ? sectionArgs[1] : 0 - Global.changeAppSectionBySectionType(section, subsection) + let subsubsection = sectionArgs.length > 2 ? sectionArgs[2] : -1 + Global.changeAppSectionBySectionType(section, subsection, subsubsection) } else Global.openLink(link) diff --git a/ui/imports/shared/views/AssetsView.qml b/ui/imports/shared/views/AssetsView.qml index 46f2de03cf..609f012249 100644 --- a/ui/imports/shared/views/AssetsView.qml +++ b/ui/imports/shared/views/AssetsView.qml @@ -1,6 +1,7 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 +import Qt.labs.settings 1.1 import StatusQ.Core 0.1 import StatusQ.Core.Theme 0.1 @@ -19,7 +20,9 @@ import shared.stores 1.0 import shared.controls 1.0 import shared.popups 1.0 -StatusScrollView { +import AppLayouts.Wallet.controls 1.0 + +ColumnLayout { id: root // expected roles: name, symbol, enabledNetworkBalance, enabledNetworkCurrencyBalance, currencyPrice, changePct24hour, communityId, communityName, communityImage @@ -28,6 +31,7 @@ StatusScrollView { property var networkConnectionStore property var overview property bool assetDetailsLaunched: false + property bool filterVisible signal assetClicked(var token) signal sendRequested(string symbol) @@ -35,15 +39,15 @@ StatusScrollView { signal switchToCommunityRequested(string communityId) signal manageTokensRequested() - contentWidth: availableWidth + spacing: 0 QtObject { id: d property int selectedAssetIndex: -1 - readonly property bool isCustomView: d.controller.hasSettings // TODO add respect other predefined orders (#12517) + readonly property bool isCustomView: cmbTokenOrder.currentValue === SortOrderComboBox.TokenOrderCustom - function symbolIsVisible(symbol) { + function tokenIsVisible(symbol, currencyBalance) { if (symbol === "ETH") // always visible return true if (!d.controller.filterAcceptsSymbol(symbol)) // explicitely hidden @@ -51,295 +55,364 @@ StatusScrollView { if (symbol === "SNT" || symbol === "STT" || symbol === "DAI") // visible by default return true // We'll receive the tokens only with non zero balance except for Eth, Dai or SNT/STT - return true + return !!currencyBalance // TODO handle UI threshold (#12611) } - readonly property var regularAssetsModel: SortFilterProxyModel { - sourceModel: root.assets - - filters: [ - ExpressionFilter { - expression: { - d.controller.settingsDirty - return d.symbolIsVisible(model.symbol) && !model.communityId - } - } - // TODO add other sort/filter using ManageTokensController (#12517) - ] - sorters: ExpressionSorter { - expression: { - d.controller.settingsDirty - return d.controller.lessThan(modelLeft.symbol, modelRight.symbol) - } - enabled: d.isCustomView - } - } - - readonly property var communityAssetsModel: SortFilterProxyModel { - sourceModel: root.assets - filters: [ - ExpressionFilter { - expression: { - d.controller.settingsDirty - return d.symbolIsVisible(model.symbol) && !!model.communityId - } - } - // TODO add other sort/filter using ManageTokensController (#12517) - ] - sorters: ExpressionSorter { - expression: { - d.controller.settingsDirty - return d.controller.lessThan(modelLeft.symbol, modelRight.symbol) - } - enabled: d.isCustomView - } - } - readonly property bool hasCommunityAssets: d.communityAssetsModel.count - readonly property var controller: ManageTokensController { settingsKey: "WalletAssets" } + readonly property bool hasCommunityAssets: communityAssetsLV.count + function hideAllCommunityTokens(communityId) { - const tokenSymbols = ModelUtils.getAll(communityAssetsModel, "symbol", "communityId", communityId) + const tokenSymbols = ModelUtils.getAll(communityAssetsLV.model, "symbol", "communityId", communityId) d.controller.settingsHideCommunityTokens(communityId, tokenSymbols) } } - ColumnLayout { - width: root.availableWidth - spacing: 0 + component CustomSFPM: SortFilterProxyModel { + id: customFilter + property bool isCommunity - StatusListView { - Layout.fillWidth: true - Layout.preferredHeight: contentHeight - interactive: false - objectName: "assetViewStatusListView" - model: d.regularAssetsModel - delegate: delegateLoader - } + sourceModel: root.assets + proxyRoles: [ + ExpressionRole { + name: "currentBalance" + expression: model.enabledNetworkBalance.amount + }, + ExpressionRole { + name: "currentCurrencyBalance" + expression: model.enabledNetworkCurrencyBalance.amount + }, + ExpressionRole { + name: "tokenPrice" + expression: model.currencyPrice.amount + } + ] + filters: [ + ExpressionFilter { + expression: { + d.controller.settingsDirty + return d.tokenIsVisible(model.symbol, model.currentCurrencyBalance) && (customFilter.isCommunity ? !!model.communityId : !model.communityId) + } + } + ] + sorters: [ + ExpressionSorter { + expression: { + d.controller.settingsDirty + return d.controller.lessThan(modelLeft.symbol, modelRight.symbol) + } + enabled: d.isCustomView + }, + RoleSorter { + roleName: cmbTokenOrder.currentSortRoleName + sortOrder: cmbTokenOrder.currentSortOrder + enabled: !d.isCustomView + } + ] + } + + Settings { + category: "AssetsViewSortSettings" + property alias currentSortField: cmbTokenOrder.currentIndex + property alias currentSortOrder: cmbTokenOrder.currentSortOrder + } + + ColumnLayout { + Layout.fillWidth: true + Layout.preferredHeight: root.filterVisible ? implicitHeight : 0 + spacing: 20 + opacity: Layout.preferredHeight < implicitHeight ? 0 : 1 + + Behavior on Layout.preferredHeight { NumberAnimation { duration: 200; easing.type: Easing.InOutQuad } } + Behavior on opacity { NumberAnimation { duration: 200; easing.type: Easing.InOutQuad } } StatusDialogDivider { Layout.fillWidth: true - Layout.topMargin: Style.current.padding - Layout.bottomMargin: Style.current.halfPadding - visible: d.hasCommunityAssets } RowLayout { Layout.fillWidth: true - Layout.leftMargin: Style.current.padding - Layout.rightMargin: Style.current.smallPadding - Layout.bottomMargin: 4 - visible: d.hasCommunityAssets + spacing: Style.current.halfPadding + StatusBaseText { - text: qsTr("Community assets") color: Theme.palette.baseColor1 + font.pixelSize: Style.current.additionalTextSize + text: qsTr("Sort by:") } - Item { Layout.fillWidth: true } - StatusFlatButton { - Layout.preferredWidth: 32 - Layout.preferredHeight: 32 - icon.name: "info" - textColor: Theme.palette.baseColor1 - horizontalPadding: 0 - verticalPadding: 0 - onClicked: Global.openPopup(communityInfoPopupCmp) + + SortOrderComboBox { + id: cmbTokenOrder + hasCustomOrderDefined: d.controller.hasSettings + model: [ + { value: SortOrderComboBox.TokenOrderCurrencyBalance, text: qsTr("Asset balance value"), icon: "token-sale", sortRoleName: "currentCurrencyBalance" }, // custom SFPM ExpressionRole on "enabledNetworkCurrencyBalance" amount + { value: SortOrderComboBox.TokenOrderBalance, text: qsTr("Asset balance"), icon: "channel", sortRoleName: "currentBalance" }, // custom SFPM ExpressionRole on "enabledNetworkBalance" amount + { value: SortOrderComboBox.TokenOrderCurrencyPrice, text: qsTr("Asset value"), icon: "token", sortRoleName: "tokenPrice" }, // custom SFPM ExpressionRole on "currencyPrice" amount + { value: SortOrderComboBox.TokenOrder1WChange, text: qsTr("1d change: balance value"), icon: "history", sortRoleName: "changePct24hour" }, // FIXME changePct1week role missing in backend!!! + { value: SortOrderComboBox.TokenOrderAlpha, text: qsTr("Asset name"), icon: "bold", sortRoleName: "name" }, + { value: SortOrderComboBox.TokenOrderCustom, text: qsTr("Custom order"), icon: "exchange", sortRoleName: "" }, + { value: SortOrderComboBox.TokenOrderNone, text: "---", icon: "", sortRoleName: "" }, // separator + { value: SortOrderComboBox.TokenOrderCreateCustom, text: hasCustomOrderDefined ? qsTr("Edit custom order →") : qsTr("Create custom order →"), + icon: "", sortRoleName: "" } + ] + onCreateOrEditRequested: { + root.manageTokensRequested() + } } } - StatusListView { + StatusDialogDivider { Layout.fillWidth: true - Layout.preferredHeight: contentHeight - interactive: false - objectName: "communityAssetViewStatusListView" - model: d.communityAssetsModel - delegate: delegateLoader } + } - Component { - id: delegateLoader - Loader { - property var modelData: model - property int delegateIndex: index - width: ListView.view.width - sourceComponent: model.loading ? loadingTokenDelegate: tokenDelegate + StatusScrollView { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.topMargin: Style.current.padding + leftPadding: 0 + verticalPadding: 0 + contentWidth: availableWidth + + ColumnLayout { + width: parent.width + spacing: 0 + + StatusListView { + id: regularAssetsLV + Layout.fillWidth: true + Layout.preferredHeight: contentHeight + interactive: false + objectName: "assetViewStatusListView" + model: CustomSFPM {} + delegate: delegateLoader } - } - Component { - id: loadingTokenDelegate - LoadingTokenDelegate { - objectName: "AssetView_LoadingTokenDelegate_" + delegateIndex + StatusDialogDivider { + Layout.fillWidth: true + Layout.topMargin: Style.current.padding + Layout.bottomMargin: Style.current.halfPadding + visible: d.hasCommunityAssets } - } - Component { - id: tokenDelegate - TokenDelegate { - objectName: "AssetView_TokenListItem_" + (!!modelData ? modelData.symbol : "") - readonly property string balance: !!modelData ? "%1".arg(modelData.enabledNetworkBalance.amount) : "" // Needed for the tests - errorTooltipText_1: !!modelData && !!networkConnectionStore ? networkConnectionStore.getBlockchainNetworkDownTextForToken(modelData.balances) : "" - errorTooltipText_2: !!networkConnectionStore ? networkConnectionStore.getMarketNetworkDownText() : "" - subTitle: { - if (!modelData) { - return "" - } - if (networkConnectionStore && networkConnectionStore.noTokenBalanceAvailable) { - return "" - } - return LocaleUtils.currencyAmountToLocaleString(modelData.enabledNetworkBalance) + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: Style.current.padding + Layout.rightMargin: Style.current.smallPadding + Layout.bottomMargin: 4 + visible: d.hasCommunityAssets + StatusBaseText { + text: qsTr("Community assets") + color: Theme.palette.baseColor1 } - errorMode: !!networkConnectionStore ? networkConnectionStore.noBlockchainConnectionAndNoCache && !networkConnectionStore.noMarketConnectionAndNoCache : false - errorIcon.tooltip.text: !!networkConnectionStore ? networkConnectionStore.noBlockchainConnectionAndNoCacheText : "" - onClicked: (itemId, mouse) => { - if (mouse.button === Qt.LeftButton) { - RootStore.getHistoricalDataForToken(modelData.symbol, RootStore.currencyStore.currentCurrency) - d.selectedAssetIndex = delegateIndex - let selectedModel = !!modelData.communityId ? d.communityAssetsModel: d.regularAssetsModel - assetClicked(selectedModel.get(delegateIndex)) - } else if (mouse.button === Qt.RightButton) { - Global.openMenu(tokenContextMenu, this, - {symbol: modelData.symbol, assetName: modelData.name, assetImage: symbolUrl, - communityId: modelData.communityId, communityName: modelData.communityName, communityImage: modelData.communityImage}) - } - } - onSwitchToCommunityRequested: root.switchToCommunityRequested(communityId) - Component.onCompleted: { - // on Model reset if the detail view is shown, update the data in background. - if(root.assetDetailsLaunched && delegateIndex === d.selectedAssetIndex) { - let selectedModel = !!modelData.communityId ? d.communityAssetsModel: d.regularAssetsModel - assetClicked(selectedModel.get(delegateIndex)) - } + Item { Layout.fillWidth: true } + StatusFlatButton { + Layout.preferredWidth: 32 + Layout.preferredHeight: 32 + icon.name: "info" + textColor: Theme.palette.baseColor1 + horizontalPadding: 0 + verticalPadding: 0 + onClicked: Global.openPopup(communityInfoPopupCmp) } } - } - Component { - id: tokenContextMenu - StatusMenu { - onClosed: destroy() - - property string symbol - property string assetName - property string assetImage - property string communityId - property string communityName - property string communityImage - - StatusAction { - enabled: root.networkConnectionStore.sendBuyBridgeEnabled && !root.overview.isWatchOnlyAccount && root.overview.canSend - icon.name: "send" - text: qsTr("Send") - onTriggered: root.sendRequested(symbol) - } - StatusAction { - icon.name: "receive" - text: qsTr("Receive") - onTriggered: root.receiveRequested(symbol) - } - StatusMenuSeparator {} - StatusAction { - icon.name: "settings" - text: qsTr("Manage tokens") - onTriggered: root.manageTokensRequested() - } - StatusAction { - enabled: symbol !== "ETH" - type: StatusAction.Type.Danger - icon.name: "hide" - text: qsTr("Hide asset") - onTriggered: Global.openPopup(confirmHideAssetPopup, {symbol, assetName, assetImage, communityId}) - } - StatusAction { - enabled: !!communityId - type: StatusAction.Type.Danger - icon.name: "hide" - text: qsTr("Hide all assets from this community") - onTriggered: Global.openPopup(confirmHideCommunityAssetsPopup, {communityId, communityName, communityImage}) - } + StatusListView { + id: communityAssetsLV + Layout.fillWidth: true + Layout.preferredHeight: contentHeight + interactive: false + objectName: "communityAssetViewStatusListView" + model: CustomSFPM { isCommunity: true } + delegate: delegateLoader } } + } - Component { - id: communityInfoPopupCmp - StatusDialog { - destroyOnClose: true - title: qsTr("What are community assets?") - standardButtons: Dialog.Ok - width: 520 - contentItem: StatusBaseText { - wrapMode: Text.Wrap - text: qsTr("Community assets are assets that have been minted by a community. As these assets cannot be verified, always double check their origin and validity before interacting with them. If in doubt, ask a trusted member or admin of the relevant community.") - } - } + Component { + id: delegateLoader + Loader { + property var modelData: model + property int delegateIndex: index + width: ListView.view.width + sourceComponent: model.loading ? loadingTokenDelegate: tokenDelegate } + } - Component { - id: confirmHideAssetPopup - ConfirmationDialog { - property string symbol - property string assetName - property string assetImage - property string communityId - - readonly property string formattedName: assetName + (communityId ? " (" + qsTr("community asset") + ")" : "") - - width: 520 - destroyOnClose: true - confirmButtonLabel: qsTr("Hide %1").arg(assetName) - cancelBtnType: "" - showCancelButton: true - headerSettings.title: qsTr("Hide %1").arg(formattedName) - headerSettings.asset.name: assetImage - confirmationText: qsTr("Are you sure you want to hide %1? You will no longer see or be able to interact with this asset anywhere inside Status.").arg(formattedName) - onCancelButtonClicked: close() - onConfirmButtonClicked: { - d.controller.settingsHideToken(symbol) - close() - Global.displayToastMessage( - qsTr("%1 was successfully hidden. You can toggle asset visibility via %2.").arg(formattedName) - .arg(`` + qsTr("Settings", "Go to Settings") + ""), - "", - "checkmark-circle", - false, - Constants.ephemeralNotificationType.success, - "" - ) - } - } + Component { + id: loadingTokenDelegate + LoadingTokenDelegate { + objectName: "AssetView_LoadingTokenDelegate_" + delegateIndex } + } - Component { - id: confirmHideCommunityAssetsPopup - ConfirmationDialog { - property string communityId - property string communityName - property string communityImage - - width: 520 - destroyOnClose: true - confirmButtonLabel: qsTr("Hide all assets minted by this community") - cancelBtnType: "" - showCancelButton: true - headerSettings.title: qsTr("Hide %1 community assets").arg(communityName) - headerSettings.asset.name: communityImage - confirmationText: qsTr("Are you sure you want to hide all community assets minted by %1? You will no longer see or be able to interact with these assets anywhere inside Status.").arg(communityName) - onCancelButtonClicked: close() - onConfirmButtonClicked: { - d.hideAllCommunityTokens(communityId) - close() - Global.displayToastMessage( - qsTr("%1 community assets were successfully hidden. You can toggle asset visibility via %2.").arg(communityName) - .arg(`` + qsTr("Settings", "Go to Settings") + ""), - "", - "checkmark-circle", - false, - Constants.ephemeralNotificationType.success, - "" - ) + Component { + id: tokenDelegate + TokenDelegate { + objectName: "AssetView_TokenListItem_" + (!!modelData ? modelData.symbol : "") + readonly property string balance: !!modelData ? "%1".arg(modelData.enabledNetworkBalance.amount) : "" // Needed for the tests + errorTooltipText_1: !!modelData && !!networkConnectionStore ? networkConnectionStore.getBlockchainNetworkDownTextForToken(modelData.balances) : "" + errorTooltipText_2: !!networkConnectionStore ? networkConnectionStore.getMarketNetworkDownText() : "" + subTitle: { + if (!modelData) { + return "" + } + if (networkConnectionStore && networkConnectionStore.noTokenBalanceAvailable) { + return "" + } + return "%1 %2".arg(LocaleUtils.stripTrailingZeroes(LocaleUtils.numberToLocaleString(modelData.enabledNetworkBalance.amount, 6), Qt.locale())) + .arg(modelData.enabledNetworkBalance.symbol) + } + errorMode: !!networkConnectionStore ? networkConnectionStore.noBlockchainConnectionAndNoCache && !networkConnectionStore.noMarketConnectionAndNoCache : false + errorIcon.tooltip.text: !!networkConnectionStore ? networkConnectionStore.noBlockchainConnectionAndNoCacheText : "" + onClicked: (itemId, mouse) => { + if (mouse.button === Qt.LeftButton) { + RootStore.getHistoricalDataForToken(modelData.symbol, RootStore.currencyStore.currentCurrency) + d.selectedAssetIndex = delegateIndex + let selectedView = !!modelData.communityId ? communityAssetsLV : regularAssetsLV + assetClicked(selectedView.model.get(delegateIndex)) + } else if (mouse.button === Qt.RightButton) { + Global.openMenu(tokenContextMenu, this, + {symbol: modelData.symbol, assetName: modelData.name, assetImage: symbolUrl, + communityId: modelData.communityId, communityName: modelData.communityName, communityImage: modelData.communityImage}) + } + } + onSwitchToCommunityRequested: root.switchToCommunityRequested(communityId) + Component.onCompleted: { + // on Model reset if the detail view is shown, update the data in background. + if(root.assetDetailsLaunched && delegateIndex === d.selectedAssetIndex) { + let selectedView = !!modelData.communityId ? communityAssetsLV : regularAssetsLV + assetClicked(selectedView.model.get(delegateIndex)) } } } } + + Component { + id: tokenContextMenu + StatusMenu { + onClosed: destroy() + + property string symbol + property string assetName + property string assetImage + property string communityId + property string communityName + property string communityImage + + StatusAction { + enabled: root.networkConnectionStore.sendBuyBridgeEnabled && !root.overview.isWatchOnlyAccount && root.overview.canSend + icon.name: "send" + text: qsTr("Send") + onTriggered: root.sendRequested(symbol) + } + StatusAction { + icon.name: "receive" + text: qsTr("Receive") + onTriggered: root.receiveRequested(symbol) + } + StatusMenuSeparator {} + StatusAction { + icon.name: "settings" + text: qsTr("Manage tokens") + onTriggered: root.manageTokensRequested() + } + StatusAction { + enabled: symbol !== "ETH" + type: StatusAction.Type.Danger + icon.name: "hide" + text: qsTr("Hide asset") + onTriggered: Global.openPopup(confirmHideAssetPopup, {symbol, assetName, assetImage, communityId}) + } + StatusAction { + enabled: !!communityId + type: StatusAction.Type.Danger + icon.name: "hide" + text: qsTr("Hide all assets from this community") + onTriggered: Global.openPopup(confirmHideCommunityAssetsPopup, {communityId, communityName, communityImage}) + } + } + } + + Component { + id: communityInfoPopupCmp + StatusDialog { + destroyOnClose: true + title: qsTr("What are community assets?") + standardButtons: Dialog.Ok + width: 520 + contentItem: StatusBaseText { + wrapMode: Text.Wrap + text: qsTr("Community assets are assets that have been minted by a community. As these assets cannot be verified, always double check their origin and validity before interacting with them. If in doubt, ask a trusted member or admin of the relevant community.") + } + } + } + + Component { + id: confirmHideAssetPopup + ConfirmationDialog { + property string symbol + property string assetName + property string assetImage + property string communityId + + readonly property string formattedName: assetName + (communityId ? " (" + qsTr("community asset") + ")" : "") + + width: 520 + destroyOnClose: true + confirmButtonLabel: qsTr("Hide %1").arg(assetName) + cancelBtnType: "" + showCancelButton: true + headerSettings.title: qsTr("Hide %1").arg(formattedName) + headerSettings.asset.name: assetImage + confirmationText: qsTr("Are you sure you want to hide %1? You will no longer see or be able to interact with this asset anywhere inside Status.").arg(formattedName) + onCancelButtonClicked: close() + onConfirmButtonClicked: { + d.controller.settingsHideToken(symbol) + close() + Global.displayToastMessage( + qsTr("%1 was successfully hidden. You can toggle asset visibility via %2.").arg(formattedName) + .arg(`` + qsTr("Settings", "Go to Settings") + ""), + "", + "checkmark-circle", + false, + Constants.ephemeralNotificationType.success, + "" + ) + } + } + } + + Component { + id: confirmHideCommunityAssetsPopup + ConfirmationDialog { + property string communityId + property string communityName + property string communityImage + + width: 520 + destroyOnClose: true + confirmButtonLabel: qsTr("Hide all assets minted by this community") + cancelBtnType: "" + showCancelButton: true + headerSettings.title: qsTr("Hide %1 community assets").arg(communityName) + headerSettings.asset.name: communityImage + confirmationText: qsTr("Are you sure you want to hide all community assets minted by %1? You will no longer see or be able to interact with these assets anywhere inside Status.").arg(communityName) + onCancelButtonClicked: close() + onConfirmButtonClicked: { + d.hideAllCommunityTokens(communityId) + close() + Global.displayToastMessage( + qsTr("%1 community assets were successfully hidden. You can toggle asset visibility via %2.").arg(communityName) + .arg(`` + qsTr("Settings", "Go to Settings") + ""), + "", + "checkmark-circle", + false, + Constants.ephemeralNotificationType.success, + "" + ) + } + } + } } diff --git a/ui/imports/utils/Constants.qml b/ui/imports/utils/Constants.qml index be989507f4..cfe3079442 100644 --- a/ui/imports/utils/Constants.qml +++ b/ui/imports/utils/Constants.qml @@ -352,6 +352,14 @@ QtObject { readonly property int backUpSeed: 17 } + readonly property QtObject walletSettingsSubsection: QtObject { + readonly property int manageNetworks: 0 + readonly property int manageAccounts: 1 + readonly property int manageAssets: 2 + readonly property int manageCollectibles: 3 + readonly property int manageTokenLists: 4 + } + readonly property QtObject currentUserStatus: QtObject{ readonly property int unknown: 0 readonly property int automatic: 1 diff --git a/ui/imports/utils/Global.qml b/ui/imports/utils/Global.qml index e8fed78a69..f03056bc9e 100644 --- a/ui/imports/utils/Global.qml +++ b/ui/imports/utils/Global.qml @@ -9,6 +9,7 @@ QtObject { property var applicationWindow property bool activityPopupOpened: false property int settingsSubsection: Constants.settingsSubsection.profile + property int settingsSubSubsection: -1 property var userProfile property bool appIsReady: false @@ -62,7 +63,7 @@ QtObject { signal activateDeepLink(string link) signal setNthEnabledSectionActive(int nthSection) - signal appSectionBySectionTypeChanged(int sectionType, int subsection) + signal appSectionBySectionTypeChanged(int sectionType, int subsection, int settingsSubsection) signal openSendModal(string address) signal switchToCommunity(string communityId) @@ -103,8 +104,8 @@ QtObject { root.openDownloadModalRequested(available, version, url); } - function changeAppSectionBySectionType(sectionType, subsection = 0) { - root.appSectionBySectionTypeChanged(sectionType, subsection); + function changeAppSectionBySectionType(sectionType, subsection = 0, settingsSubsection = -1) { + root.appSectionBySectionTypeChanged(sectionType, subsection, settingsSubsection) } function openMenu(menuComponent, menuParent, params = {}, point = undefined) {