diff --git a/storybook/pages/AssetsViewNewPage.qml b/storybook/pages/AssetsViewNewPage.qml new file mode 100644 index 000000000..9ec15b497 --- /dev/null +++ b/storybook/pages/AssetsViewNewPage.qml @@ -0,0 +1,271 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import shared.views 1.0 +import utils 1.0 + +import Storybook 1.0 + +import Qt.labs.settings 1.1 + +SplitView { + id: root + + ListModel { + id: assetsModel + + function format(amount, symbol) { + return `${amount.toLocaleString(Qt.locale())} ${symbol}` + } + + Component.onCompleted: { + const data = [ + { + key: "key_ETH", + symbol: "ETH", + name: "Ether", + icon: Constants.tokenIcon("ETH", false), + balance: 10.0, + balanceText: format(10.0, "ETH"), + error: "", + + marketDetailsAvailable: true, + marketDetailsLoading: true, + marketPrice: 0, + marketChangePct24hour: 0, + + communityId: "", + communityName: "", + communityIcon: Qt.resolvedUrl(""), + + position: 2, + canBeHidden: false + }, + { + key: "key_SNT", + symbol: "SNT", + name: "Status", + icon: Constants.tokenIcon("SNT", false), + balance: 20023.0, + balanceText: format(20023.0, "SNT"), + error: "", + + marketDetailsAvailable: true, + marketDetailsLoading: false, + marketPrice: 50.23, + marketChangePct24hour: 12, + + communityId: "", + communityName: "", + communityIcon: Qt.resolvedUrl(""), + + position: 1, + canBeHidden: true + }, + { + key: "key_MCT", + symbol: "MCT", + name: "My custom token", + icon: Constants.tokenIcon("ZRX", false), + balance: 102.4, + balanceText: format(102.4, "MCT"), + error: "", + + marketDetailsAvailable: false, + marketDetailsLoading: false, + marketPrice: 0, + marketChangePct24hour: 0, + + communityId: "34", + communityName: "Crypto Kitties", + communityIcon: Constants.tokenIcon("DAI", false), + + position: 4, + canBeHidden: true + }, + { + key: "key_DAI", + symbol: "DAI", + name: "Dai", + icon: Constants.tokenIcon("DAI", false), + balance: 123.24, + balanceText: format(123.24, "DAI"), + error: "", + + marketDetailsAvailable: true, + marketDetailsLoading: false, + marketPrice: 23.23, + marketChangePct24hour: 2.3, + + communityId: "", + communityName: "", + communityIcon: Qt.resolvedUrl(""), + + position: 3, + canBeHidden: true + }, + { + key: "key_USDT", + symbol: "USDT", + name: "USDT", + icon: Constants.tokenIcon("USDT", false), + balance: 15.24, + balanceText: format(15.24, "USDT"), + error: "", + + marketDetailsAvailable: true, + marketDetailsLoading: false, + marketPrice: 0.99, + marketChangePct24hour: 0, + + communityId: "", + communityName: "", + communityIcon: Qt.resolvedUrl(""), + + position: 5, + canBeHidden: true + }, + { + key: "key_TBT", + symbol: "TBT", + name: "The best token", + icon: Constants.tokenIcon("UNI", false), + balance: 102, + balanceText: format(102, "TBT"), + error: "Pocket Network (POKT) & Infura are currently both " + + "unavailable for %1. %1 balances are as of %2." + .arg("TBT").arg("10/06/2024"), + + marketDetailsAvailable: false, + marketDetailsLoading: false, + marketPrice: 0, + marketChangePct24hour: 0, + + communityId: "3423", + communityName: "Best tokens", + communityIcon: Constants.tokenIcon("UNI", false), + + position: 6, + canBeHidden: true + } + ] + + append(data) + } + } + + SplitView { + SplitView.fillWidth: true + SplitView.fillHeight: true + + orientation: Qt.Vertical + + Pane { + SplitView.fillWidth: true + SplitView.fillHeight: true + + AssetsViewNew { + anchors.fill: parent + + loading: loadingCheckBox.checked + sorterVisible: sorterVisibleCheckBox.checked + customOrderAvailable: customOrderAvailableCheckBox.checked + + sendEnabled: sendEnabledCheckBox.checked + swapEnabled: swapEnabledCheckBox.checked + swapVisible: swapVisibleCheckBox.checked + + balanceError: balanceErrorCheckBox.checked + ? "Balance error!" : "" + + marketDataError: marketDataErrorCheckBox.checked + ? "Market data error!" : "" + + model: assetsModel + + onSendRequested: logs.logEvent(`send requested: ${key}`) + onReceiveRequested: logs.logEvent(`receive requested: ${key}`) + onSwapRequested: logs.logEvent(`swap requested: ${key}`) + onAssetClicked: logs.logEvent(`asset clicked: ${key}`) + + onHideRequested: logs.logEvent(`hide requested: ${key}`) + onHideCommunityAssets: logs.logEvent(`hide community assets requested: ${communityKey}`) + onManageTokensRequested: logs.logEvent(`manage tokens requested`) + } + } + + Logs { + id: logs + } + + LogsView { + clip: true + + SplitView.preferredHeight: 150 + SplitView.fillWidth: true + + logText: logs.logText + } + } + + Pane { + SplitView.preferredWidth: 300 + + ColumnLayout { + CheckBox { + id: loadingCheckBox + + text: "loading" + } + CheckBox { + id: sorterVisibleCheckBox + + text: "sorter visible" + } + CheckBox { + id: customOrderAvailableCheckBox + + text: "custom order available" + } + CheckBox { + id: sendEnabledCheckBox + + text: "send enabled" + } + CheckBox { + id: swapEnabledCheckBox + + text: "swap enabled" + } + CheckBox { + id: swapVisibleCheckBox + + text: "swap visible" + } + CheckBox { + id: balanceErrorCheckBox + + text: "balance error" + } + CheckBox { + id: marketDataErrorCheckBox + + text: "market data error" + } + } + } + + Settings { + property alias loading: loadingCheckBox.checked + property alias filterVisible: sorterVisibleCheckBox.checked + property alias customOrderAvailable: customOrderAvailableCheckBox.checked + property alias sendEnabled: sendEnabledCheckBox.checked + property alias swapEnabled: swapEnabledCheckBox.checked + property alias swapVisible: swapVisibleCheckBox.checked + property alias balanceError: balanceErrorCheckBox.checked + property alias marketDataError: marketDataErrorCheckBox.checked + } +} + +// category: Views diff --git a/storybook/pages/TokenDelegatePage.qml b/storybook/pages/TokenDelegatePage.qml new file mode 100644 index 000000000..2f81f44fc --- /dev/null +++ b/storybook/pages/TokenDelegatePage.qml @@ -0,0 +1,167 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import Storybook 1.0 + +import shared.controls 1.0 +import utils 1.0 + +SplitView { + id: root + + Item { + SplitView.fillWidth: true + SplitView.fillHeight: true + + TokenDelegateNew { + anchors.centerIn: parent + + name: nameTextFiled.text + balance: balanceTextFiled.text + icon: Constants.tokenIcon(iconTextFiled.text, false) + + marketDetailsAvailable: marketDataAvailableCheckBox.checked + marketDetailsLoading: marketDataLoadingCheckBox.checked + marketCurrencyPrice: marketCurrencyPriceTextFiled.text + marketBalance: marketBalanceTextFiled.text + marketChangePct24hour: market24ChangeSpinBox.value + + communityId: communityCheckBox.checked ? "42" : "" + communityName: communityNameTextField.text + communityIcon: Constants.tokenIcon("DAI", false) + + errorTooltipText_1: errorTooltipTextField.text + errorTooltipText_2: marketDataErrorTooltipTextField.text + + onCommunityClicked: { + console.log("community clicked:", communityId) + } + } + } + + Pane { + ColumnLayout { + RowLayout { + Label { + text: "name:" + } + TextField { + id: nameTextFiled + text: "Ether" + } + } + RowLayout { + Label { + text: "icon:" + } + TextField { + id: iconTextFiled + text: "ETH" + } + } + RowLayout { + Label { + text: "balance:" + } + TextField { + id: balanceTextFiled + text: "0,1232 ETH" + } + } + RowLayout { + CheckBox { + id: marketDataAvailableCheckBox + text: "market data available" + checked: true + } + CheckBox { + id: marketDataLoadingCheckBox + text: "market data loading" + checked: false + } + } + RowLayout { + Label { + text: "market balance:" + } + TextField { + id: marketBalanceTextFiled + text: "711,23 USD" + } + } + RowLayout { + Label { + text: "market currency price:" + } + TextField { + id: marketCurrencyPriceTextFiled + text: "3 823,23 USD" + } + } + RowLayout { + Label { + text: "Market data error tooltip:" + } + TextField { + id: marketDataErrorTooltipTextField + text: "" + } + } + RowLayout { + Label { + text: "market 24 change:" + } + Slider { + id: market24ChangeSpinBox + + value: 0.1 + + from: -100 + to: 200 + } + + RoundButton { + text: "0" + + onClicked: market24ChangeSpinBox.value = 0 + } + } + CheckBox { + id: communityCheckBox + text: "community minted" + checked: false + } + RowLayout { + Label { + text: "community id:" + } + TextField { + id: communityNameTextField + text: "Crypto Kitties" + } + } + RowLayout { + Label { + text: "Error tooltip:" + } + TextField { + id: errorTooltipTextField + text: "" + } + } + Label { + visible: communityCheckBox.checked + && marketDataAvailableCheckBox.checked + text: "Community pill and market details are not expected \n" + + "to occur both for a single token." + color: "red" + } + Item { + Layout.fillHeight: true + } + } + } +} + +// category: Controls diff --git a/ui/app/AppLayouts/Wallet/controls/SortOrderComboBox.qml b/ui/app/AppLayouts/Wallet/controls/SortOrderComboBox.qml index e479a5127..f422507d3 100644 --- a/ui/app/AppLayouts/Wallet/controls/SortOrderComboBox.qml +++ b/ui/app/AppLayouts/Wallet/controls/SortOrderComboBox.qml @@ -35,7 +35,9 @@ ComboBox { currentIndex = d.currentIndex root.createOrEditRequested() } else { - if (d.currentIndex === index) // just keep the same sort role and flip the up/down + if (index === indexOfValue(SortOrderComboBox.TokenOrderCustom)) + currentSortOrder = Qt.AscendingOrder + 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 @@ -211,16 +213,22 @@ ComboBox { } delegate: ItemDelegate { + id: menuDelegate + required property int index required property var modelData + readonly property bool isSeparator: text === "---" - id: menuDelegate width: ListView.view.width highlighted: root.highlightedIndex === index enabled: !isSeparator + + readonly property bool custom: + modelData["value"] === SortOrderComboBox.TokenOrderCustom + visible: { - if (modelData["value"] === SortOrderComboBox.TokenOrderCustom) // hide "Custom order" menu entry if none defined + if (custom) // hide "Custom order" menu entry if none defined return root.hasCustomOrderDefined return true } @@ -252,7 +260,7 @@ ComboBox { readonly property int menuIndex: menuDelegate.index readonly property string menuText: menuDelegate.text readonly property string iconName: menuDelegate.icon.name - readonly property bool showUpDownArrows: menuDelegate.modelData["sortRoleName"] !== "" + readonly property bool showUpDownArrows: !menuDelegate.custom && menuDelegate.modelData["sortRoleName"] !== "" readonly property bool isEditAction: modelData["value"] === SortOrderComboBox.TokenOrderCreateCustom sourceComponent: menuDelegate.isSeparator ? separatorMenuComponent : regularMenuComponent } diff --git a/ui/imports/shared/controls/AssetsSectionDelegate.qml b/ui/imports/shared/controls/AssetsSectionDelegate.qml index 468a0ce46..fef6e10c2 100644 --- a/ui/imports/shared/controls/AssetsSectionDelegate.qml +++ b/ui/imports/shared/controls/AssetsSectionDelegate.qml @@ -1,5 +1,5 @@ -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.Core.Theme 0.1 @@ -11,9 +11,10 @@ import utils 1.0 ColumnLayout { id: root - signal openInfoPopup() property alias text: sectionTitle.text + signal infoButtonClicked + spacing: 0 StatusDialogDivider { @@ -39,7 +40,7 @@ ColumnLayout { textColor: Theme.palette.baseColor1 horizontalPadding: 0 verticalPadding: 0 - onClicked: openInfoPopup() + onClicked: root.infoButtonClicked() } } } diff --git a/ui/imports/shared/controls/LoadingTokenDelegateNew.qml b/ui/imports/shared/controls/LoadingTokenDelegateNew.qml new file mode 100644 index 000000000..1aa24db72 --- /dev/null +++ b/ui/imports/shared/controls/LoadingTokenDelegateNew.qml @@ -0,0 +1,16 @@ +import utils 1.0 + +TokenDelegateNew { + title: Constants.dummyText + subTitle: Constants.dummyText + asset.name: Constants.dummyText + + statusListItemSubTitle.loading: true + statusListItemTitle.loading: true + statusListItemIcon.loading: true + + marketDetailsAvailable: true + marketDetailsLoading: true + + enabled: false +} diff --git a/ui/imports/shared/controls/TokenDelegateNew.qml b/ui/imports/shared/controls/TokenDelegateNew.qml new file mode 100644 index 000000000..636983c01 --- /dev/null +++ b/ui/imports/shared/controls/TokenDelegateNew.qml @@ -0,0 +1,169 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +import StatusQ.Core.Theme 0.1 +import StatusQ.Components 0.1 +import StatusQ.Core 0.1 +import StatusQ.Controls 0.1 + +import AppLayouts.Wallet.controls 1.0 +import utils 1.0 + +StatusListItem { + id: root + + property string name + property url icon + property string balance + + property bool marketDetailsAvailable: false + property string marketBalance + property bool marketDetailsLoading: false + property string marketCurrencyPrice + property real marketChangePct24hour + + property string communityId + property string communityName + property url communityIcon + + property string errorTooltipText_1 + property string errorTooltipText_2 + + signal communityClicked(string communityId) + + QtObject { + id: d + + readonly property bool isCommunityToken: !!root.communityId + + readonly property string textColor: { + if (!root.marketDetailsAvailable) + return Theme.palette.successColor1 + + if (root.marketChangePct24hour === 0) + return Theme.palette.baseColor1 + + return root.marketChangePct24hour < 0 + ? Theme.palette.dangerColor1 + : Theme.palette.successColor1 + } + + readonly property string upDownTriangle: { + if (root.marketChangePct24hour === 0) + return "" + + return root.marketChangePct24hour < 0 ? "▾" : "▴" + } + } + + title: root.name + subTitle: root.balance + asset.name: root.icon + asset.isImage: true + asset.width: 32 + asset.height: 32 + errorIcon.tooltip.maxWidth: 300 + height: implicitHeight + + statusListItemTitleIcons.sourceComponent: StatusFlatRoundButton { + width: 14 + height: visible ? 14 : 0 + icon.width: 14 + icon.height: 14 + icon.name: "tiny/warning" + icon.color: Theme.palette.dangerColor1 + tooltip.text: root.errorTooltipText_1 + tooltip.maxWidth: 300 + visible: !!tooltip.text + } + + components: [ + Column { + anchors.verticalCenter: parent.verticalCenter + StatusFlatRoundButton { + id: errorIcon + width: 14 + height: visible ? 14 : 0 + icon.width: 14 + icon.height: 14 + icon.name: "tiny/warning" + icon.color: Theme.palette.dangerColor1 + tooltip.text: root.errorTooltipText_2 + tooltip.maxWidth: 200 + visible: root.marketDetailsAvailable && !!tooltip.text + } + StatusTextWithLoadingState { + id: currencyBalance + + anchors.right: parent.right + visible: !errorIcon.visible && root.marketDetailsAvailable + + loading: root.marketDetailsLoading + text: loading ? Constants.dummyText : root.marketBalance + } + Row { + anchors.right: parent.right + spacing: 6 + visible: !errorIcon.visible && root.marketDetailsAvailable + + StatusTextWithLoadingState { + id: change24HourPercentageText + + anchors.verticalCenter: parent.verticalCenter + customColor: d.textColor + font.pixelSize: 13 + loading: root.marketDetailsLoading + + text: qsTr("%1 %2%", "[up/down/none character depending on value sign] [localized percentage value]%") + .arg(d.upDownTriangle).arg(LocaleUtils.numberToLocaleString(root.marketChangePct24hour, 2)) + } + Rectangle { + anchors.verticalCenter: parent.verticalCenter + width: 1 + height: 12 + color: Theme.palette.directColor9 + } + StatusTextWithLoadingState { + id: currencyPrice + + anchors.verticalCenter: parent.verticalCenter + customColor: d.textColor + font.pixelSize: 13 + loading: root.marketDetailsLoading + text: loading ? Constants.dummyText : root.marketCurrencyPrice + } + } + + Loader { + active: d.isCommunityToken + + sourceComponent: ManageTokensCommunityTag { + anchors.right: parent.right + + communityImage: root.communityIcon + communityName: root.communityName + communityId: root.communityId + + asset.letterSize: 12 + + TapHandler { + acceptedButtons: Qt.LeftButton + onSingleTapped: root.communityClicked(root.communityId) + } + } + } + } + ] + + states: State { + name: "unknownToken" + when: !root.icon.toString() + + PropertyChanges { + target: root.asset + isLetterIdenticon: true + color: Theme.palette.miscColor5 + name: root.name + } + } +} diff --git a/ui/imports/shared/controls/qmldir b/ui/imports/shared/controls/qmldir index 8c23fb410..4731d6593 100644 --- a/ui/imports/shared/controls/qmldir +++ b/ui/imports/shared/controls/qmldir @@ -22,6 +22,7 @@ InformationTag 1.0 InformationTag.qml InformationTile 1.0 InformationTile.qml Input 1.0 Input.qml LoadingTokenDelegate 1.0 LoadingTokenDelegate.qml +LoadingTokenDelegateNew 1.0 LoadingTokenDelegateNew.qml LinkPreviewDebugView 1.0 LinkPreviewDebugView.qml ProfilePerspectiveSelector 1.0 ProfilePerspectiveSelector.qml RadioButtonSelector 1.0 RadioButtonSelector.qml @@ -43,6 +44,7 @@ StyledTextEditWithLoadingState 1.0 StyledTextEditWithLoadingState.qml StyledTextField 1.0 StyledTextField.qml Timer 1.0 Timer.qml TokenDelegate 1.0 TokenDelegate.qml +TokenDelegateNew 1.0 TokenDelegateNew.qml TransactionAddress 1.0 TransactionAddress.qml TransactionAddressTile 1.0 TransactionAddressTile.qml TransactionDataTile 1.0 TransactionDataTile.qml diff --git a/ui/imports/shared/popups/send/panels/HoldingSelector.qml b/ui/imports/shared/popups/send/panels/HoldingSelector.qml index 3cb300107..db4684678 100644 --- a/ui/imports/shared/popups/send/panels/HoldingSelector.qml +++ b/ui/imports/shared/popups/send/panels/HoldingSelector.qml @@ -246,7 +246,7 @@ Item { width: ListView.view.width required property bool section text: Helpers.assetsSectionTitle(section, holdingItemSelector.hasCommunityTokens, d.isBrowsingGroup, d.isCurrentBrowsingTypeAsset) - onOpenInfoPopup: Global.openPopup(communityInfoPopupCmp) + onInfoButtonClicked: Global.openPopup(communityInfoPopupCmp) } comboBoxControl.popup.onOpened: comboBoxControl.popup.contentItem.headerItem.focusSearch() comboBoxControl.popup.onClosed: comboBoxControl.popup.contentItem.headerItem.clear() diff --git a/ui/imports/shared/popups/send/views/TokenListView.qml b/ui/imports/shared/popups/send/views/TokenListView.qml index ca602f0db..0fa40d830 100644 --- a/ui/imports/shared/popups/send/views/TokenListView.qml +++ b/ui/imports/shared/popups/send/views/TokenListView.qml @@ -175,7 +175,7 @@ Item { width: parent.width height: !!text ? 52 : 0 // if we bind to some property instead of hardcoded value it wont work nice when switching tabs or going inside collection and back text: Helpers.assetsSectionTitle(section, tokenList.hasCommunityTokens, d.isBrowsingGroup, d.isBrowsingTypeERC20) - onOpenInfoPopup: Global.openPopup(communityInfoPopupCmp) + onInfoButtonClicked: Global.openPopup(communityInfoPopupCmp) } } } diff --git a/ui/imports/shared/views/AssetContextMenu.qml b/ui/imports/shared/views/AssetContextMenu.qml new file mode 100644 index 000000000..26e0648da --- /dev/null +++ b/ui/imports/shared/views/AssetContextMenu.qml @@ -0,0 +1,61 @@ +import QtQuick 2.15 + +import StatusQ.Popups 0.1 + +StatusMenu { + id: root + + property bool sendEnabled: true + property bool swapEnabled: true + + property bool swapVisible: true + property bool hideVisible: true + property bool communityHideVisible: true + + signal sendRequested + signal receiveRequested + signal swapRequested + signal hideRequested + signal communityHideRequested + signal manageTokensRequested + + StatusAction { + enabled: root.sendEnabled + visibleOnDisabled: true + icon.name: "send" + text: qsTr("Send") + onTriggered: root.sendRequested() + } + StatusAction { + icon.name: "receive" + text: qsTr("Receive") + onTriggered: root.receiveRequested() + } + StatusAction { + icon.name: "swap" + text: qsTr("Swap") + enabled: root.swapEnabled && root.swapVisible + visibleOnDisabled: root.swapVisible + onTriggered: root.swapRequested() + } + StatusMenuSeparator {} + StatusAction { + icon.name: "settings" + text: qsTr("Manage tokens") + onTriggered: root.manageTokensRequested() + } + StatusAction { + enabled: root.hideVisible + type: StatusAction.Type.Danger + icon.name: "hide" + text: qsTr("Hide asset") + onTriggered: root.hideRequested() + } + StatusAction { + enabled: root.communityHideVisible + type: StatusAction.Type.Danger + icon.name: "hide" + text: qsTr("Hide all assets from this community") + onTriggered: root.communityHideRequested() + } +} diff --git a/ui/imports/shared/views/AssetsView.qml b/ui/imports/shared/views/AssetsView.qml index be2d47a6f..3dd94b196 100644 --- a/ui/imports/shared/views/AssetsView.qml +++ b/ui/imports/shared/views/AssetsView.qml @@ -250,7 +250,7 @@ ColumnLayout { AssetsSectionDelegate { width: parent.width text: qsTr("Community minted") - onOpenInfoPopup: Global.openPopup(communityInfoPopupCmp) + onInfoButtonClicked: Global.openPopup(communityInfoPopupCmp) } } diff --git a/ui/imports/shared/views/AssetsViewNew.qml b/ui/imports/shared/views/AssetsViewNew.qml new file mode 100644 index 000000000..3a5360aae --- /dev/null +++ b/ui/imports/shared/views/AssetsViewNew.qml @@ -0,0 +1,333 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import QtQml.Models 2.15 + +import StatusQ 0.1 +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Popups.Dialog 0.1 + +import AppLayouts.Wallet.controls 1.0 +import shared.controls 1.0 +import shared.popups 1.0 +import utils 1.0 + +import SortFilterProxyModel 0.2 + + +Control { + id: root + + /** + Expected model structure: + + key [string] - unique identifier of a token, e.g "0x3234235" + symbol [string] - token's symbol e.g. "ETH" or "SNT" + name [string] - token's name e.g. "Ether" or "Dai" + icon [url] - token's icon url + balance [double] - tokens balance is the commonly used unit, e.g. 1.2 for 1.2 ETH, used + for sorting and computing market value + balanceText [string] - formatted and localized balance. This is not done internally because + it may depend on many external factors + error [string] - error message related to balance + + marketDetailsAvailable [bool] - specifies if market datails are available for given token + marketDetailsLoading [bool] - specifies if market datails are available for given token + marketPrice [double] - specifies market price in currently used currency + marketChangePct24hour [double] - percentage price change in last 24 hours, e.g. 0.5 for 0.5% of price change + + communityId [string] - for community assets, unique identifier of a community, e.g. "0x6734235" + communityName [string] - for community assets, name of a community e.g. "Crypto Kitties" + communityIcon [url] - for community assets, community's icon url + + position [int] - if custom order available, display position defined by the user via token management + canBeHidden [bool] - specifies if given token can be hidden (e.g. ETH should be always visible) + **/ + property var model + + // enables global loading state useful when real data are not yet available + property bool loading + + // shows/hides list sorter + property bool sorterVisible + + // allows/disables choosing custom sort order from a sorter + property bool customOrderAvailable + + // switches configuring right click menu + property bool sendEnabled: true + property bool swapEnabled: true + property bool swapVisible: true + + property string balanceError + + // global market data error, presented for all tokens expecting market data + property string marketDataError + + // formatting function for fiat currency values + property var formatFiat: balance => `${balance.toLocaleString(Qt.locale())} XYZ` + + signal sendRequested(string key) + signal receiveRequested(string key) + signal swapRequested(string key) + signal assetClicked(string key) + signal communityClicked(string communityKey) + signal hideRequested(string key) + signal hideCommunityAssets(string communityKey) + signal manageTokensRequested + + QtObject { + id: d + + readonly property int loadingItemsCount: 25 + } + + SortFilterProxyModel { + id: sfpm + + sourceModel: root.model + + proxyRoles: [ + // helper role for rendering section delegate + FastExpressionRole { + name: "isCommunity" + expression: !!communityId ? "community" : "" + expectedRoles: ["communityId"] + }, + FastExpressionRole { + name: "marketBalance" + expression: balance * marketPrice + expectedRoles: ["balance", "marketPrice"] + }, + FastExpressionRole { + name: "change1DayFiat" + expression: marketBalance * (1 - (1 / (marketChangePct24hour / 100 + 1))) + expectedRoles: ["marketBalance", "marketChangePct24hour"] + } + ] + + sorters: [ + RoleSorter { + roleName: "isCommunity" + }, + RoleSorter { + roleName: sortOrderComboBox.currentSortRoleName + sortOrder: sortOrderComboBox.currentSortOrder + } + ] + } + + contentItem: ColumnLayout { + ColumnLayout { + Layout.fillHeight: false + Layout.preferredHeight: root.sorterVisible ? implicitHeight : 0 + + opacity: root.sorterVisible ? 1 : 0 + spacing: 20 + visible: opacity > 0 + + 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 } + + RowLayout { + Layout.fillWidth: true + Layout.fillHeight: false + + spacing: Style.current.halfPadding + + StatusBaseText { + color: Theme.palette.baseColor1 + font.pixelSize: Style.current.additionalTextSize + text: qsTr("Sort by:") + } + + SortOrderComboBox { + id: sortOrderComboBox + + objectName: "cmbTokenOrder" + hasCustomOrderDefined: root.customOrderAvailable + + model: [ + { value: SortOrderComboBox.TokenOrderCurrencyBalance, + text: qsTr("Asset balance value"), icon: "", sortRoleName: "marketBalance" }, + { value: SortOrderComboBox.TokenOrderBalance, + text: qsTr("Asset balance"), icon: "", sortRoleName: "balance" }, + { value: SortOrderComboBox.TokenOrderCurrencyPrice, + text: qsTr("Asset value"), icon: "", sortRoleName: "marketPrice" }, + { value: SortOrderComboBox.TokenOrder1DChange, + text: qsTr("1d change: balance value"), icon: "", sortRoleName: "change1DayFiat" }, + { value: SortOrderComboBox.TokenOrderAlpha, + text: qsTr("Asset name"), icon: "", sortRoleName: "name" }, + { value: SortOrderComboBox.TokenOrderCustom, + text: qsTr("Custom order"), icon: "", sortRoleName: "position" }, + { 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() + } + } + } + + StatusDialogDivider { Layout.fillWidth: true } + } + + DelegateModel { + id: regularModel + + model: sfpm + + delegate: TokenDelegateNew { + objectName: `AssetView_TokenListItem_${model.symbol}` + + width: ListView.view.width + + name: model.name + icon: model.icon + balance: model.balanceText + marketBalance: root.formatFiat(model.marketBalance) + + marketDetailsAvailable: model.marketDetailsAvailable + marketDetailsLoading: model.marketDetailsLoading + marketCurrencyPrice: root.formatFiat(model.change1DayFiat) + marketChangePct24hour: model.marketChangePct24hour + + communityId: model.communityId + communityName: model.communityName ?? "" + communityIcon: model.communityIcon ?? "" + + errorTooltipText_1: model.error + errorTooltipText_2: root.marketDataError + + errorMode: !!root.balanceError + errorIcon.tooltip.text: root.balanceError + + onClicked: { + if (mouse.button === Qt.LeftButton) + root.assetClicked(model.key) + else if (mouse.button === Qt.RightButton) + tokenContextMenu.createObject(this, { model }).popup(mouse) + } + + onCommunityClicked: root.communityClicked(model.communityId) + } + } + + DelegateModel { + id: loadingModel + + model: d.loadingItemsCount + + delegate: LoadingTokenDelegateNew { + objectName: `AssetView_LoadingTokenDelegate_${model.index}` + + width: ListView.view.width + } + } + + StatusListView { + id: listView + + objectName: "assetViewStatusListView" + + Layout.fillWidth: true + Layout.fillHeight: true + + model: root.loading ? loadingModel : regularModel + + section { + property: "isCommunity" + delegate: AssetsSectionDelegate { + width: parent.width + text: qsTr("Community minted") + onInfoButtonClicked: communityInfoPopup.createObject(this).open() + } + } + } + } + + Component { + id: tokenContextMenu + + AssetContextMenu { + required property var model + + readonly property string key: model.key + readonly property string communityKey: model.communityId + + onClosed: destroy() + + sendEnabled: root.sendEnabled + swapEnabled: root.swapEnabled + swapVisible: root.swapVisible + hideVisible: model.canBeHidden + communityHideVisible: !!model.isCommunity + + onSendRequested: root.sendRequested(key) + onReceiveRequested: root.receiveRequested(key) + onSwapRequested: root.swapRequested(key) + + onHideRequested: + confirmHideAssetPopup.createObject(parent, { model }).open() + onCommunityHideRequested: + confirmHideCommunityAssetsPopup.createObject(parent, { model }).open() + + onManageTokensRequested: root.manageTokensRequested() + } + } + + Component { + id: communityInfoPopup + + CommunityAssetsInfoPopup { + destroyOnClose: true + } + } + + Component { + id: confirmHideAssetPopup + + ConfirmHideAssetPopup { + destroyOnClose: true + + required property var model + + symbol: model.symbol + name: model.name + icon: model.icon + + onConfirmButtonClicked: { + root.hideRequested(model.key) + close() + } + } + } + + Component { + id: confirmHideCommunityAssetsPopup + + ConfirmHideCommunityAssetsPopup { + required property var model + + name: model.communityName + icon: model.communityIcon + + onConfirmButtonClicked: { + root.hideCommunityAssets(model.communityId) + close(); + } + } + } +} diff --git a/ui/imports/shared/views/ConfirmHideAssetPopup.qml b/ui/imports/shared/views/ConfirmHideAssetPopup.qml new file mode 100644 index 000000000..046d62a3f --- /dev/null +++ b/ui/imports/shared/views/ConfirmHideAssetPopup.qml @@ -0,0 +1,19 @@ +import shared.popups 1.0 + +ConfirmationDialog { + required property string symbol + required property string name + required property string icon + + width: 520 + + confirmButtonLabel: qsTr("Hide asset") + cancelBtnType: "" + showCancelButton: true + headerSettings.title: qsTr("Hide %1 (%2)").arg(name).arg(symbol) + headerSettings.asset.name: icon + confirmationText: qsTr("Are you sure you want to hide %1 (%2)? You will no longer see or be able to interact with this asset anywhere inside Status.") + .arg(name).arg(symbol) + + onCancelButtonClicked: close() +} diff --git a/ui/imports/shared/views/ConfirmHideCommunityAssetsPopup.qml b/ui/imports/shared/views/ConfirmHideCommunityAssetsPopup.qml new file mode 100644 index 000000000..534c7fe82 --- /dev/null +++ b/ui/imports/shared/views/ConfirmHideCommunityAssetsPopup.qml @@ -0,0 +1,17 @@ +import shared.popups 1.0 + +ConfirmationDialog { + required property string name + required property string icon + + width: 520 + + confirmButtonLabel: qsTr("Hide '%1' assets").arg(name) + cancelBtnType: "" + showCancelButton: true + headerSettings.title: qsTr("Hide %1 community assets").arg(name) + headerSettings.asset.name: icon + 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(name) + + onCancelButtonClicked: close() +} diff --git a/ui/imports/shared/views/qmldir b/ui/imports/shared/views/qmldir index 489bc311e..a8b9bedcd 100644 --- a/ui/imports/shared/views/qmldir +++ b/ui/imports/shared/views/qmldir @@ -1,16 +1,20 @@ +AssetContextMenu 1.0 AssetContextMenu.qml AssetsView 1.0 AssetsView.qml +AssetsViewNew 1.0 AssetsViewNew.qml +ConfirmHideAssetPopup 1.0 ConfirmHideAssetPopup.qml +ConfirmHideCommunityAssetsPopup 1.0 ConfirmHideCommunityAssetsPopup.qml EnsResolver 1.0 EnsResolver.qml ExistingContacts 1.0 ExistingContacts.qml HistoryView 1.0 HistoryView.qml NoFriendsRectangle 1.0 NoFriendsRectangle.qml -PasswordView 1.0 PasswordView.qml PasswordConfirmationView 1.0 PasswordConfirmationView.qml +PasswordView 1.0 PasswordView.qml PickedContacts 1.0 PickedContacts.qml ProfileDialogView 1.0 ProfileDialogView.qml SearchResults 1.0 SearchResults.qml -TransactionSigner 1.0 TransactionSigner.qml +SyncingCodeInstructions 1.0 SyncingCodeInstructions.qml SyncingDeviceView 1.0 SyncingDeviceView.qml SyncingDisplayCode 1.0 SyncingDisplayCode.qml -SyncingErrorMessage 1.0 SyncingErrorMessage.qml -SyncingCodeInstructions 1.0 SyncingCodeInstructions.qml SyncingEnterCode 1.0 SyncingEnterCode.qml +SyncingErrorMessage 1.0 SyncingErrorMessage.qml +TransactionSigner 1.0 TransactionSigner.qml