feat(WalletSettings): Display token list as well as popup to see each token in it

- It creates new component `SupportedTokensListsPanel`.
- It updates tokens list tab content with the expected one.
- It creates new component `TokenListPopup`.
- It adds support to new created components in `storybook`.

Closes #12374
This commit is contained in:
Noelia 2023-10-25 12:40:10 +02:00 committed by Noelia
parent b1064644e8
commit e0179bb2b5
12 changed files with 820 additions and 11 deletions

View File

@ -0,0 +1,71 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import Storybook 1.0
import Models 1.0
import SortFilterProxyModel 0.2
import StatusQ.Core.Theme 0.1
import AppLayouts.Profile.panels 1.0
import AppLayouts.Profile.stores 1.0
import StatusQ 0.1
SplitView {
id: root
readonly property var sourcesOfTokensModel: SourceOfTokensModel {}
readonly property var flatTokensModel: FlatTokensModel {}
readonly property var joinModel: LeftJoinModel {
leftModel: root.flatTokensModel
rightModel: NetworksModel.allNetworks
joinRole: "chainId"
}
readonly property var tokensProxyModel: SortFilterProxyModel {
sourceModel: joinModel
proxyRoles: [
ExpressionRole {
name: "explorerUrl"
expression: { return "https://status.im/" }
},
ExpressionRole {
name: "jsArraySources"
expression: model.sources.split(";")
}
]
}
orientation: Qt.Vertical
Logs { id: logs }
Pane {
SplitView.fillWidth: true
SplitView.fillHeight: true
SupportedTokenListsPanel {
anchors.fill: parent
sourcesOfTokensModel: root.sourcesOfTokensModel
tokensListModel: root.tokensProxyModel
onItemClicked: logs.logEvent("SupportedTokenListsPanel::onItemClicked --> Key --> " + key)
}
}
LogsAndControlsPanel {
id: logsAndControlsPanel
SplitView.minimumHeight: 100
SplitView.preferredHeight: 200
logsView.logText: logs.logText
}
}
// category: Panels
// https://www.figma.com/file/FkFClTCYKf83RJWoifWgoX/Wallet-v2?type=design&node-id=18057%3A239410&mode=design&t=zSZ650alzNvE28GO-1

View File

@ -0,0 +1,148 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import Storybook 1.0
import Models 1.0
import SortFilterProxyModel 0.2
import StatusQ 0.1
import AppLayouts.Profile.popups 1.0
SplitView {
id: root
Logs { id: logs }
readonly property var sourcesOfTokensModel: SourceOfTokensModel {}
readonly property var flatTokensModel: FlatTokensModel {}
readonly property var joinModel: LeftJoinModel {
leftModel: root.flatTokensModel
rightModel: NetworksModel.allNetworks
joinRole: "chainId"
}
readonly property var tokensProxyModel: SortFilterProxyModel {
sourceModel: joinModel
proxyRoles: [
ExpressionRole {
name: "explorerUrl"
expression: { return "https://status.im/" }
},
ExpressionRole {
name: "jsArraySources"
expression: model.sources.split(";")
}
]
}
SplitView {
orientation: Qt.Vertical
SplitView.fillWidth: true
Item {
SplitView.fillWidth: true
SplitView.fillHeight: true
PopupBackground {
anchors.fill: parent
}
Instantiator {
model: SortFilterProxyModel {
sourceModel: sourcesOfTokensModel
filters: ValueFilter {
id: keyFilter
roleName: "key"
value : uniswapBtn.checked ? "uniswap" : "status"
}
}
delegate: QtObject {
id: delegate
required property string name
required property string image
required property string source
required property int updatedAt
required property string version
required property int tokensCount
readonly property TokenListPopup popup: TokenListPopup {
parent: root
visible: true
modal: false
closePolicy: Popup.NoAutoClose
sourceName: delegate.name
sourceImage: delegate.image
sourceUrl: delegate.source
sourceUpdatedAt: delegate.updatedAt
sourceVersion: delegate.version
tokensCount: delegate.tokensCount
tokensListModel: SortFilterProxyModel {
sourceModel: root.tokensProxyModel
// Filter by source
filters: ExpressionFilter {
expression: model.jsArraySources.includes(keyFilter.value)
}
}
onLinkClicked: logs.logEvent("TokenListPopup::onLinkClicked --> " + link)
onClosed: keyFilter.value = ""
Component.onCompleted: open()
}
}
}
}
LogsAndControlsPanel {
id: logsAndControlsPanel
SplitView.minimumHeight: 100
SplitView.preferredHeight: 150
logsView.logText: logs.logText
}
}
Pane {
SplitView.minimumWidth: 300
SplitView.preferredWidth: 300
Column {
spacing: 12
Label {
text: "Token List:"
font.bold: true
}
RadioButton {
id: uniswapBtn
text: "Uniswap"
checked: true
onCheckedChanged: keyFilter.value = "uniswap"
}
RadioButton {
id: statusBtn
text: "Status"
onCheckedChanged: keyFilter.value = "status"
}
}
}
}
// category: Popups
// https://www.figma.com/file/FkFClTCYKf83RJWoifWgoX/Wallet-v2?type=design&node-id=18057%3A239798&mode=design&t=Vnm5GS8EZFLpeRAY-1

View File

@ -0,0 +1,141 @@
import QtQuick 2.15
import Models 1.0
ListModel {
readonly property string uniswap: "uniswap" //SourceOfTokensModel.uniswap
readonly property string status: "status" //SourceOfTokensModel.status
readonly property string custom: "custom" //SourceOfTokensModel.custom
readonly property var data: [
{
key: "0",
name: "Unisocks",
symbol: "SOCKS",
sources: uniswap + ";" + status,
chainId: NetworksModel.ethNet,
address: "0x0000000000000000000000000000000000000123",
decimals: "18",
image: ModelsData.assets.socks,
type: 1,
communityId: "",
description: "",
websiteUrl: ""
},
{
key: "1",
name: "Unisocks",
symbol: "SOCKS",
sources: uniswap + ";" + status,
chainId: NetworksModel.optimismNet,
address: "0x00000000000000000000000000000000000ade21",
decimals: "18",
image: ModelsData.assets.socks,
type: 1,
communityId: "",
description: "",
websiteUrl: ""
},
{
key: "2",
name: "Ox",
symbol: "ZRX",
sources: uniswap + ";" + status,
chainId: NetworksModel.ethNet,
address: "0x1230000000000000000000000000000000000123",
decimals: "18",
image: ModelsData.assets.zrx,
type: 1,
communityId: "",
description: "",
websiteUrl: ""
},
{
key: "3",
name: "1inch",
symbol: "1INCH",
sources: uniswap + ";" + status,
chainId: NetworksModel.ethNet,
address: "0x4321000000000000000000000000000000000123",
decimals: "18",
image: ModelsData.assets.inch,
type: 1,
communityId: "",
description: "",
websiteUrl: ""
},
{
key: "4",
name: "Aave",
symbol: "AAVE",
sources: uniswap + ";" + status,
chainId: NetworksModel.arbitrumNet,
address: "0x6543000000000000000000000000000000000123",
decimals: "18",
image: ModelsData.assets.aave,
type: 1,
communityId: "",
description: "",
websiteUrl: ""
},
{
key: "5",
name: "Amp",
symbol: "AMP",
sources: uniswap,
chainId: NetworksModel.arbitrumNet,
address: "0x6543700000000000000000000000000000000123",
decimals: "18",
image: ModelsData.assets.amp,
type: 1,
communityId: "",
description: "",
websiteUrl: ""
},
{
key: "6",
name: "Dai",
symbol: "DAI",
sources: uniswap,
chainId: NetworksModel.optimismNet,
address: "0xabc2000000000000000000000000000000000123",
decimals: "18",
image: ModelsData.assets.dai,
type: 1,
communityId: "",
description: "",
websiteUrl: ""
},
{
key: "7",
name: "snt",
symbol: "SNT",
sources: status,
chainId: NetworksModel.optimismNet,
address: "0xbbc2000000000000000000000000000000000123",
decimals: "18",
image: ModelsData.assets.snt,
type: 1,
communityId: "",
description: "",
websiteUrl: ""
},
{
key: "8",
name: "snt",
symbol: "SNT",
sources: status,
chainId: NetworksModel.ethNet,
address: "0xbbc200000000000000000000000000000000abcd",
decimals: "18",
image: ModelsData.assets.snt,
type: 1,
communityId: "",
description: "",
websiteUrl: ""
}
]
Component.onCompleted: append(data)
}

View File

@ -3,14 +3,35 @@ pragma Singleton
import QtQuick 2.15
QtObject {
id: root
readonly property int ethNet: 1
readonly property int optimismNet: 2
readonly property int arbitrumNet: 3
readonly property int optimismNet: 10
readonly property int arbitrumNet: 42161
readonly property int hermezNet: 4
readonly property int testnetNet: 5
readonly property int customNet: 6
function getChainName(chainId) {
if(chainId === root.ethNet)
return "Mainnet"
if(chainId === root.optimismNet)
return "Optimism"
if(chainId === root.arbitrumNet)
return "Arbitrum"
if(chainId === root.hermezNet)
return "Hermez"
if(chainId === root.testnetNet)
return "Goerli"
if(chainId === root.customNet)
return "Custom"
}
component CustomNetworkModel: ListModel {
// Simulate Nim's way of providing access to data
function rowData(index, propName) {

View File

@ -0,0 +1,34 @@
import QtQuick 2.15
import Models 1.0
ListModel {
id: root
readonly property string uniswap: "uniswap"
readonly property string status: "status"
readonly property string custom: "custom"
readonly property var data: [
{
key: root.uniswap,
name: "Uniswap Labs Default",
updatedAt: 1695720962,
source: "https://gateway.ipfs.io/ipns/tokens.uniswap.org",
version: "11.6.0",
tokensCount: 731,
image: ModelsData.assets.uni
},
{
key: root.status,
name: "Status Token List",
updatedAt: 1661506562,
source: "https://status.im/",
version: "11.6.0",
tokensCount: 250,
image: ModelsData.assets.snt
}
]
Component.onCompleted: append(data)
}

View File

@ -5,18 +5,21 @@ BannerModel 1.0 BannerModel.qml
ChannelsModel 1.0 ChannelsModel.qml
CollectiblesModel 1.0 CollectiblesModel.qml
FeesModel 1.0 FeesModel.qml
FlatTokensModel 1.0 FlatTokensModel.qml
IconModel 1.0 IconModel.qml
LinkPreviewModel 1.0 LinkPreviewModel.qml
MintedTokensModel 1.0 MintedTokensModel.qml
RecipientModel 1.0 RecipientModel.qml
SourceOfTokensModel 1.0 SourceOfTokensModel.qml
TokenHoldersModel 1.0 TokenHoldersModel.qml
UsersModel 1.0 UsersModel.qml
WalletSendAccountsModel 1.0 WalletSendAccountsModel.qml
WalletAccountsModel 1.0 WalletAccountsModel.qml
WalletAssetsModel 1.0 WalletAssetsModel.qml
WalletCollectiblesModel 1.0 WalletCollectiblesModel.qml
WalletKeyPairModel 1.0 WalletKeyPairModel.qml
WalletNestedCollectiblesModel 1.0 WalletNestedCollectiblesModel.qml
singleton ModelsData 1.0 ModelsData.qml
singleton NetworksModel 1.0 NetworksModel.qml
singleton PermissionsModel 1.0 PermissionsModel.qml
WalletSendAccountsModel 1.0 WalletSendAccountsModel.qml

View File

@ -0,0 +1,108 @@
import QtQuick 2.15
import QtQuick.Layouts 1.15
import StatusQ.Core 0.1
import StatusQ.Components 0.1
import StatusQ.Controls 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Core.Utils 0.1 as SQUtils
import SortFilterProxyModel 0.2
import shared.controls 1.0
import utils 1.0
import AppLayouts.Profile.popups 1.0
StatusListView {
id: root
required property var sourcesOfTokensModel // Expected roles: key, name, updatedAt, source, version, tokensCount, image
required property var tokensListModel // Expected roles: name, symbol, image, chainName, explorerUrl
signal itemClicked(string key)
model: root.sourcesOfTokensModel
spacing: 8
delegate: StatusListItem {
height: 76
width: parent.width
title: model.name
subTitle: qsTr("%n token(s) · Last updated %1 @%2",
"",
model.tokensCount).arg(LocaleUtils.formatDate(model.updatedAt * 1000)).arg(LocaleUtils.formatTime(model.updatedAt, Locale.ShortFormat))
asset.name: model.image
asset.isImage: true
border.width: 1
border.color: Theme.palette.baseColor5
components: [
StatusButton {
text: qsTr("View")
onClicked: keyFilter.value = model.key
}
]
}
footer: Item {
width: parent.width
height: root.count > 0 ? shapeRect.implicitHeight + 40 : shapeRect.implicitHeight
ShapeRectangle {
id: shapeRect
anchors.bottom: parent.bottom
width: parent.width - 4 // The rectangular path is rendered outside
icon: "add"
text: qsTr("Add Token List (coming soon)")
}
}
Instantiator {
model: SortFilterProxyModel {
sourceModel: sourcesOfTokensModel
filters: ValueFilter {
id: keyFilter
roleName: "key"
value : ""
}
}
delegate: QtObject {
id: delegate
required property string name
required property string image
required property string source
required property int updatedAt
required property string version
required property int tokensCount
readonly property TokenListPopup popup: TokenListPopup {
parent: root
sourceName: delegate.name
sourceImage: delegate.image
sourceUrl: delegate.source
sourceUpdatedAt: delegate.updatedAt
sourceVersion: delegate.version
tokensCount: delegate.tokensCount
tokensListModel: SortFilterProxyModel {
sourceModel: root.tokensListModel
// Filter by source
filters: ExpressionFilter {
expression: (model.jsArraySources).includes(keyFilter.value)
}
}
onLinkClicked: Global.openLink(link)
onClosed: keyFilter.value = ""
Component.onCompleted: open()
}
}
}
}

View File

@ -3,3 +3,4 @@ ProfileShowcaseCommunitiesPanel 1.0 ProfileShowcaseCommunitiesPanel.qml
ProfileShowcaseCollectiblesPanel 1.0 ProfileShowcaseCollectiblesPanel.qml
ProfileShowcaseAccountsPanel 1.0 ProfileShowcaseAccountsPanel.qml
ProfileShowcaseAssetsPanel 1.0 ProfileShowcaseAssetsPanel.qml
SupportedTokenListsPanel 1.0 SupportedTokenListsPanel.qml

View File

@ -0,0 +1,273 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQml.Models 2.15
import QtQuick.Layouts 1.15
import StatusQ.Core 0.1
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import StatusQ.Popups.Dialog 0.1
import StatusQ.Core.Theme 0.1
import utils 1.0
import shared.panels 1.0
StatusDialog {
id: root
required property string sourceName
required property string sourceImage
required property string sourceUrl
required property int sourceUpdatedAt
required property string sourceVersion
required property int tokensCount
required property var tokensListModel // Expected roles: name, symbol, image, chainName, explorerUrl
signal linkClicked(string link)
QtObject {
id: d
readonly property int symbolColumnWidth: 90
readonly property int addressColumnWidth: 106
readonly property int externalLinkBtnWidth: 32
}
width: 521 // by design
padding: 0
horizontalPadding: Style.current.padding
contentItem: StatusListView {
id: list
topMargin: Style.current.padding
bottomMargin: Style.current.padding
implicitHeight: contentHeight
model: root.tokensListModel
header: ColumnLayout {
spacing: 20
width: list.width
CustomSourceInfoComponent {
Layout.fillWidth: true
Layout.margins: Style.current.padding
}
Separator {}
CustomHeaderDelegate {}
}
delegate: CustomDelegate {}
}
header: StatusDialogHeader {
headline.title: qsTr("%1 Token List").arg(root.sourceName)
headline.subtitle: qsTr("%n token(s)", "", root.tokensCount)
actions.closeButton.onClicked: root.close()
leftComponent: StatusSmartIdenticon {
asset.name: root.sourceImage
asset.isImage: !!asset.name
}
}
footer: StatusDialogFooter {
spacing: Style.current.padding
rightButtons: ObjectModel {
StatusButton {
text: qsTr("Done")
type: StatusBaseButton.Type.Normal
onClicked: close()
}
}
}
component CustomTextBlock: ColumnLayout {
id: textBlock
property string title
property string text
Layout.fillWidth: true
StatusBaseText {
Layout.fillWidth: true
text: textBlock.title
}
StatusBaseText {
Layout.fillWidth: true
text: textBlock.text
color: Theme.palette.baseColor1
}
}
component CustomExternalLinkButton: StatusFlatButton {
id: extButton
property string link
Layout.preferredHeight: d.externalLinkBtnWidth
Layout.preferredWidth: d.externalLinkBtnWidth
spacing: 0
textColor: Theme.palette.baseColor1
textHoverColor: Theme.palette.directColor1
icon.name: "external-link"
onClicked: root.linkClicked(root.sourceUrl)
}
component CustomSourceInfoComponent: ColumnLayout {
spacing: 20
RowLayout {
spacing: Style.current.padding
CustomTextBlock {
title: qsTr("Source")
text: root.sourceUrl
}
CustomExternalLinkButton {
Layout.rightMargin: Style.current.halfPadding
link: root.sourceUrl
}
}
CustomTextBlock {
title: qsTr("Version")
text: root.sourceVersion
}
CustomTextBlock {
title: qsTr("Automatically updates")
text: qsTr("Last updated %n day(s) ago",
"",
LocaleUtils.daysBetween(root.sourceUpdatedAt * 1000, Date.now()))
}
}
component CustomHeaderDelegate: RowLayout {
height: 34
width: contentItem.width
spacing: 0
StatusBaseText {
Layout.fillWidth: true
Layout.leftMargin: Style.current.padding
text: qsTr("Name")
color: Theme.palette.baseColor1
}
StatusBaseText {
Layout.leftMargin: Style.current.padding
Layout.preferredWidth: d.symbolColumnWidth - Layout.leftMargin
Layout.alignment: Qt.AlignLeft
text: qsTr("Symbol")
color: Theme.palette.baseColor1
}
StatusBaseText {
Layout.leftMargin: Style.current.padding
Layout.preferredWidth: d.addressColumnWidth - Layout.leftMargin
Layout.alignment: Qt.AlignLeft
text: qsTr("Address")
color: Theme.palette.baseColor1
}
// Just a filler corresponding to external link column
Item {
Layout.leftMargin: Style.current.padding
Layout.preferredWidth: d.externalLinkBtnWidth
Layout.rightMargin: Style.current.bigPadding
}
}
component CustomDelegate: Rectangle {
width: contentItem.width
height: 64
color: (sensor.containsMouse || externalLinkBtn.hovered) ? Theme.palette.baseColor2 : "transparent"
radius: 8
MouseArea {
id: sensor
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
}
RowLayout {
spacing: 0
anchors.fill: parent
RowLayout {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.leftMargin: Style.current.padding
spacing: Style.current.padding
StatusSmartIdenticon {
asset.isImage: true
asset.name: model.image
}
ColumnLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
spacing: 0
StatusBaseText {
Layout.fillWidth: true
text: model.name
elide: Text.ElideMiddle
}
StatusBaseText {
Layout.fillWidth: true
text: model.chainName
elide: Text.ElideMiddle
color: Theme.palette.baseColor1
}
}
}
StatusBaseText {
Layout.leftMargin: Style.current.padding
Layout.preferredWidth: d.symbolColumnWidth - Layout.leftMargin
Layout.alignment: Qt.AlignLeft
text: model.symbol
}
StatusBaseText {
Layout.leftMargin: Style.current.padding
Layout.preferredWidth: d.addressColumnWidth - Layout.leftMargin
Layout.alignment: Qt.AlignLeft
text: model.address
elide: Text.ElideMiddle
}
CustomExternalLinkButton {
id: externalLinkBtn
Layout.leftMargin: Style.current.padding
Layout.rightMargin: Style.current.bigPadding
link: model.explorerUrl
}
}
}
}

View File

@ -3,7 +3,8 @@ SetupSyncingPopup 1.0 SetupSyncingPopup.qml
AddSocialLinkModal 1.0 AddSocialLinkModal.qml
ModifySocialLinkModal 1.0 ModifySocialLinkModal.qml
RenameKeypairPopup 1.0 RenameKeypairPopup.qml
WalletKeypairAccountMenu 1.0 WalletKeypairAccountMenu.qml
WalletAddressMenu 1.0 WalletAddressMenu.qml
RenameAccontModal 1.0 RenameAccontModal.qml
RemoveKeypairPopup 1.0 RemoveKeypairPopup.qml
TokenListPopup 1.0 TokenListPopup.qml
WalletKeypairAccountMenu 1.0 WalletKeypairAccountMenu.qml
WalletAddressMenu 1.0 WalletAddressMenu.qml

View File

@ -219,6 +219,9 @@ SettingsContentBase {
Layout.fillWidth: true
Layout.leftMargin: Style.current.padding
Layout.rightMargin: Style.current.padding
sourcesOfTokensModel: undefined//tokensStore.sourcesOfTokensModel
tokensListModel: undefined//tokensStore.flatTokensModel
}
DappPermissionsView {

View File

@ -5,9 +5,14 @@ import StatusQ.Controls 0.1
import shared.controls 1.0
import AppLayouts.Profile.panels 1.0
ColumnLayout {
id: root
required property var sourcesOfTokensModel // Expected roles: key, name, updatedAt, source, version, tokensCount, image
required property var tokensListModel // Expected roles: name, symbol, image, chainName, explorerUrl
StatusTabBar {
id: tabBar
@ -54,12 +59,12 @@ ColumnLayout {
text: qsTr("Youll be able to manage the display of your collectibles here")
}
// TO BE REPLACED: Empty placeholder when no token lists; dashed rounded rectangle
ShapeRectangle {
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: parent.width - 4 // The rectangular path is rendered outside
Layout.preferredHeight: 44
text: qsTr("Token List (coming soon)")
SupportedTokenListsPanel {
Layout.fillWidth: true
Layout.fillHeight: true
sourcesOfTokensModel: root.sourcesOfTokensModel
tokensListModel: root.tokensListModel
}
}
}