feat(token mgmt): update main wallet view layout with community assets

- update the AssetsView.qml view and delegates according to the latest
design
- add AssetsView to Storybook
- add new section for Community Assets
- (re)use the community badge with tooltip and link action to take the
user to the respective community
- add Community Assets info icon + popup
- create context menu for token delegates with actions
(Send/Receive/Manage tokens/Hide)
- add confirmation popups when hiding a single or all community tokens
- emit a toast bubble after hiding the token(s)
- plus related controller/backend methods for handling the
settings-related actions
- some smaller fixes/cleanups

Fixes #12369
Fixes #12372
This commit is contained in:
Lukáš Tinkl 2023-11-22 20:58:02 +01:00 committed by Lukáš Tinkl
parent 8e0db2e666
commit 3b60506460
17 changed files with 624 additions and 147 deletions

View File

@ -0,0 +1,56 @@
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import StatusQ.Core 0.1
import mainui 1.0
import utils 1.0
import shared.views 1.0
import Storybook 1.0
import Models 1.0
SplitView {
id: root
Logs { id: logs }
orientation: Qt.Horizontal
ManageTokensModel {
id: assetsModel
}
Popups {
popupParent: root
rootStore: QtObject {}
communityTokensStore: QtObject {}
}
AssetsView {
id: assetsView
SplitView.preferredWidth: 600
SplitView.fillHeight: true
assets: assetsModel
onAssetClicked: logs.logEvent("onAssetClicked", ["token"], [token.symbol, token.communityId])
onSendRequested: logs.logEvent("onSendRequested", ["symbol"], arguments)
onReceiveRequested: logs.logEvent("onReceiveRequested", ["symbol"], arguments)
onSwitchToCommunityRequested: logs.logEvent("onSwitchToCommunityRequested", ["communityId"], arguments)
onManageTokensRequested: logs.logEvent("onManageTokensRequested")
}
LogsAndControlsPanel {
id: logsAndControlsPanel
SplitView.minimumWidth: 150
SplitView.preferredWidth: 250
logsView.logText: logs.logText
}
}
// category: Views
// https://www.figma.com/file/FkFClTCYKf83RJWoifWgoX/Wallet-v2?type=design&node-id=17159-67977&mode=design&t=s5EXsh6Vi4nTNYUh-0
// https://www.figma.com/file/FkFClTCYKf83RJWoifWgoX/Wallet-v2?type=design&node-id=17171-285559&mode=design&t=s5EXsh6Vi4nTNYUh-0

View File

@ -44,6 +44,11 @@ ListModel {
symbol: "EUR",
displayDecimals: 2
},
currencyPrice: {
amount: 10.37,
symbol: "EUR",
displayDecimals: 2
},
communityId: "ddls",
communityName: "Doodles",
communityImage: ModelsData.collectibles.doodles // FIXME backend
@ -166,6 +171,11 @@ ListModel {
symbol: "EUR",
displayDecimals: 2
},
currencyPrice: {
amount: 1480.113406237,
symbol: "EUR",
displayDecimals: 2
},
changePct24hour: -3.51,
communityId: "",
communityName: "",
@ -185,15 +195,21 @@ ListModel {
enabledNetworkBalance: ({
displayDecimals: true,
stripTrailingZeroes: true,
amount: 324343.3,
amount: 0,
symbol: "SNT"
}),
enabledNetworkCurrencyBalance: ({
displayDecimals: 4,
stripTrailingZeroes: true,
amount: 2.333321323400,
amount: 0,
symbol: "EUR"
}),
currencyPrice: {
amount: 1.40627,
symbol: "EUR",
displayDecimals: 2
},
changePct24hour: 1.3,
symbol: "SNT",
name: "Status",
communityId: "",

View File

@ -2,4 +2,5 @@ import QtQuick 2.15
QtObject {
property var blockchainNetworksDown: []
property bool sendBuyBridgeEnabled: true
}

View File

@ -11,7 +11,7 @@ QtObject {
property var currentCurrency
property bool neverAskAboutUnfurlingAgain: false
property var currencyStore
property var currencyStore: CurrenciesStore {}
property var history
property var getNetworkIcon
@ -37,4 +37,8 @@ QtObject {
console.log("STUB: setNeverAskAboutUnfurlingAgain:", value)
neverAskAboutUnfurlingAgain = value
}
function getHistoricalDataForToken(symbol, currency) {
console.log("STUB: getHistoricalDataForToken:", symbol, currency)
}
}

View File

@ -27,6 +27,11 @@ public:
Q_INVOKABLE QVariant get(QAbstractItemModel *model, int row,
const QString &roleName) const;
Q_INVOKABLE QVariantList getAll(QAbstractItemModel* model,
const QString& roleName,
const QString& filterRoleName,
const QVariant& filterValue) const;
Q_INVOKABLE bool contains(QAbstractItemModel *model, const QString &roleName, const QVariant &value, int mode = Qt::CaseSensitive) const;
///< performs a strict check whether @lhs and @rhs arrays (QList<T>) contain the same elements;

View File

@ -116,6 +116,7 @@ Row {
id: textLayout
width: !iconOrImage.active ? parent.width :
parent.width - iconOrImage.width - parent.spacing
anchors.verticalCenter: parent.verticalCenter
Row {
id: headerTitleRow
width: parent.width

View File

@ -60,6 +60,32 @@ QVariant ModelUtilsInternal::get(QAbstractItemModel *model,
return {};
}
QVariantList ModelUtilsInternal::getAll(QAbstractItemModel* model,
const QString& roleName,
const QString& filterRoleName,
const QVariant& filterValue) const
{
if (!model || filterValue.isNull())
return {};
const auto role = roleByName(model, roleName);
if (role == -1)
return {};
const auto filterRole = roleByName(model, filterRoleName);
if (filterRole == -1)
return {};
QVariantList result;
const auto size = model->rowCount();
for (auto i = 0; i < size; i++) {
const auto srcIndex = model->index(i, 0);
if (srcIndex.data(filterRole) == filterValue)
result.append(srcIndex.data(role));
}
return result;
}
bool ModelUtilsInternal::contains(QAbstractItemModel* model,
const QString& roleName,
const QVariant& value,

View File

@ -110,7 +110,7 @@ QHash<int, QByteArray> RolesRenamingModel::roleNames() const
return {};
}
if (renameMap.size()) {
if (!renameMap.isEmpty()) {
qWarning().nospace()
<< "RolesRenamingModel: specified source roles not found: "
<< renameMap.keys() << "!";

View File

@ -1,6 +1,7 @@
#include "managetokenscontroller.h"
#include <QElapsedTimer>
#include <QMutableHashIterator>
ManageTokensController::ManageTokensController(QObject* parent)
: QObject(parent)
@ -112,18 +113,24 @@ void ManageTokensController::showHideGroup(const QString& groupId, bool flag)
rebuildCommunityTokenGroupsModel();
}
void ManageTokensController::saveSettings()
void ManageTokensController::saveSettings(bool reuseCurrent)
{
Q_ASSERT(!m_settingsKey.isEmpty());
setSettingsDirty(true);
if (m_arrangeByCommunity)
m_communityTokensModel->applySort();
// gather the data to save
SerializedTokenData result;
for (auto model: {m_regularTokensModel, m_communityTokensModel})
result.insert(model->save());
result.insert(m_hiddenTokensModel->save(false));
if (reuseCurrent) {
result = m_settingsData;
} else {
for(auto model : {m_regularTokensModel, m_communityTokensModel})
result.insert(model->save());
result.insert(m_hiddenTokensModel->save(false));
}
// save to QSettings
m_settings.beginGroup(settingsGroupName());
@ -144,6 +151,8 @@ void ManageTokensController::saveSettings()
// unset dirty
for (auto model: m_allModels)
model->setDirty(false);
setSettingsDirty(false);
}
void ManageTokensController::clearSettings()
@ -182,6 +191,13 @@ void ManageTokensController::loadSettings()
m_settings.endGroup();
}
void ManageTokensController::setSettingsDirty(bool dirty)
{
if (m_settingsDirty == dirty) return;
m_settingsDirty = dirty;
emit settingsDirtyChanged(m_settingsDirty);
}
void ManageTokensController::revert()
{
parseSourceModel();
@ -199,6 +215,40 @@ bool ManageTokensController::hasSettings() const
return !groups.isEmpty() && groups.contains(settingsGroupName());
}
void ManageTokensController::settingsHideToken(const QString& symbol)
{
if (m_settingsData.contains(symbol)) { // find or create the settings entry
auto [pos, visible, group] = m_settingsData.value(symbol);
m_settingsData.remove(symbol); // remove all
m_settingsData.insert(symbol, {pos, false, group});
} else {
m_settingsData.insert(symbol, {0, false, QString()});
}
saveSettings(true);
}
void ManageTokensController::settingsHideCommunityTokens(const QString& communityId, const QStringList& symbols)
{
QMutableHashIterator<QString, std::tuple<int, bool, QString>> i(m_settingsData);
bool found = false;
while (i.hasNext()) {
i.next();
const auto groupID = std::get<2>(i.value());
if (groupID == communityId) {
found = true;
i.setValue({0, false, communityId});
}
}
if (!found) {
for (const auto& symbol: symbols)
m_settingsData.insert(symbol, {0, false, communityId});
}
saveSettings(true);
}
bool ManageTokensController::lessThan(const QString& lhsSymbol, const QString& rhsSymbol) const
{
int leftPos, rightPos;
@ -216,7 +266,7 @@ bool ManageTokensController::lessThan(const QString& lhsSymbol, const QString& r
bool ManageTokensController::filterAcceptsSymbol(const QString& symbol) const
{
const auto& [pos, visible, groupId] = m_settingsData.value(symbol, {INT_MAX, false, QString()});
const auto& [pos, visible, groupId] = m_settingsData.value(symbol, {INT_MAX, true, QString()});
return visible;
}

View File

@ -14,9 +14,9 @@ class ManageTokensController : public QObject, public QQmlParserStatus
Q_INTERFACES(QQmlParserStatus)
// input properties
Q_PROPERTY(QAbstractItemModel* sourceModel READ sourceModel WRITE setSourceModel NOTIFY sourceModelChanged FINAL REQUIRED)
Q_PROPERTY(QAbstractItemModel* sourceModel READ sourceModel WRITE setSourceModel NOTIFY sourceModelChanged FINAL)
Q_PROPERTY(QString settingsKey READ settingsKey WRITE setSettingsKey NOTIFY settingsKeyChanged FINAL REQUIRED)
Q_PROPERTY(bool arrangeByCommunity READ arrangeByCommunity WRITE setArrangeByCommunity NOTIFY arrangeByCommunityChanged FINAL)
Q_PROPERTY(bool arrangeByCommunity READ arrangeByCommunity WRITE setArrangeByCommunity NOTIFY arrangeByCommunityChanged FINAL) // TODO persist in settings
// output properties
Q_PROPERTY(QAbstractItemModel* regularTokensModel READ regularTokensModel CONSTANT FINAL)
@ -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 settingsDirty READ settingsDirty NOTIFY settingsDirtyChanged FINAL)
public:
explicit ManageTokensController(QObject* parent = nullptr);
@ -33,11 +34,15 @@ public:
Q_INVOKABLE void showHideCommunityToken(int row, bool flag);
Q_INVOKABLE void showHideGroup(const QString& groupId, bool flag);
Q_INVOKABLE void saveSettings();
Q_INVOKABLE void loadSettings();
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);
Q_INVOKABLE bool lessThan(const QString& lhsSymbol, const QString& rhsSymbol) const;
Q_INVOKABLE bool filterAcceptsSymbol(const QString& symbol) const;
@ -50,6 +55,7 @@ signals:
void dirtyChanged();
void arrangeByCommunityChanged();
void settingsKeyChanged();
void settingsDirtyChanged(bool dirty);
private:
QAbstractItemModel* m_sourceModel{nullptr};
@ -60,16 +66,16 @@ private:
void addItem(int index);
ManageTokensModel* m_regularTokensModel{nullptr};
QAbstractItemModel* regularTokensModel() const { return m_regularTokensModel; };
QAbstractItemModel* regularTokensModel() const { return m_regularTokensModel; }
ManageTokensModel* m_communityTokensModel{nullptr};
QAbstractItemModel* communityTokensModel() const { return m_communityTokensModel; };
QAbstractItemModel* communityTokensModel() const { return m_communityTokensModel; }
ManageTokensModel* m_communityTokenGroupsModel{nullptr};
QAbstractItemModel* communityTokenGroupsModel() const { return m_communityTokenGroupsModel; };
QAbstractItemModel* communityTokenGroupsModel() const { return m_communityTokenGroupsModel; }
ManageTokensModel* m_hiddenTokensModel{nullptr};
QAbstractItemModel* hiddenTokensModel() const { return m_hiddenTokensModel; };
QAbstractItemModel* hiddenTokensModel() const { return m_hiddenTokensModel; }
bool dirty() const;
@ -88,8 +94,11 @@ private:
QString settingsGroupName() const;
void setSettingsKey(const QString& newSettingsKey);
QSettings m_settings;
void loadSettings();
SerializedTokenData m_settingsData; // symbol -> {sortOrder, visible, groupId}
bool m_settingsDirty{false};
bool settingsDirty() const { return m_settingsDirty; }
void setSettingsDirty(bool dirty);
bool m_modelConnectionsInitialized{false};
};

View File

@ -63,7 +63,7 @@ SettingsContentBase {
onSaveChangesClicked: {
manageTokensView.saveChanges()
Global.displayToastMessage(
qsTr("Your new custom asset order has been applied to your %1", "Go to Wallet")
qsTr("Your new custom token order has been applied to your %1", "Go to Wallet")
.arg(`<a style="text-decoration:none" href="#${Constants.appSection.wallet}">` + qsTr("Wallet", "Go to Wallet") + "</a>"),
"",
"checkmark-circle",

View File

@ -23,7 +23,7 @@ ColumnLayout {
return false
if (tabBar.currentIndex > d.collectiblesTabIndex)
return false
if (tabBar.currentIndex === d.collectiblesTabIndex && baseCollectiblesModel.isFetching)
if (tabBar.currentIndex === d.collectiblesTabIndex && baseWalletCollectiblesModel.isFetching)
return false
return loader.item && loader.item.dirty
}
@ -51,14 +51,14 @@ ColumnLayout {
if (tabBar.currentIndex !== collectiblesTabIndex)
return
// If there is no more items to load or we're already fetching, return
if (!root.baseCollectiblesModel.hasMore || root.baseCollectiblesModel.isFetching)
if (!root.baseWalletCollectiblesModel.hasMore || root.baseWalletCollectiblesModel.isFetching)
return
root.baseCollectiblesModel.loadMore()
root.baseWalletCollectiblesModel.loadMore()
}
}
Connections {
target: root.baseCollectiblesModel
target: root.baseWalletCollectiblesModel
function onHasMoreChanged() {
d.checkLoadMoreCollectibles()
}

View File

@ -30,7 +30,8 @@ Item {
function resetView() {
stack.currentIndex = 0
root.currentTabIndex = 0
historyView.resetView()
if (walletTabBar.currentIndex === 2)
mainViewLoader.item.resetView()
}
function resetStack() {
@ -94,7 +95,6 @@ Item {
StatusTabBar {
id: walletTabBar
objectName: "rightSideWalletTabBar"
horizontalPadding: Style.current.padding
Layout.fillWidth: true
Layout.topMargin: Style.current.padding
@ -116,41 +116,67 @@ Item {
RootStore.setCurrentViewedHoldingType(walletTabBar.currentIndex === 1 ? Constants.TokenType.ERC721 : Constants.TokenType.ERC20)
}
}
StackLayout {
Loader {
id: mainViewLoader
Layout.fillWidth: true
Layout.fillHeight: true
Layout.topMargin: Style.current.padding
Layout.bottomMargin: Style.current.padding
currentIndex: walletTabBar.currentIndex
sourceComponent: {
switch (walletTabBar.currentIndex) {
case 0: return assetsView
case 1: return collectiblesView
case 2: return historyView
}
}
active: visible
AssetsView {
assets: RootStore.assets
networkConnectionStore: root.networkConnectionStore
assetDetailsLaunched: stack.currentIndex === 2
onAssetClicked: {
assetDetailView.token = token
RootStore.setCurrentViewedHolding(token.symbol, Constants.TokenType.ERC20)
stack.currentIndex = 2
Component {
id: assetsView
AssetsView {
assets: RootStore.assets
overview: RootStore.overview
networkConnectionStore: root.networkConnectionStore
assetDetailsLaunched: stack.currentIndex === 2
onAssetClicked: {
assetDetailView.token = token
RootStore.setCurrentViewedHolding(token.symbol, Constants.TokenType.ERC20)
stack.currentIndex = 2
}
onSendRequested: (symbol) => {
root.sendModal.preSelectedSendType = Constants.SendType.Transfer
root.sendModal.preSelectedHoldingID = symbol
root.sendModal.preSelectedHoldingType = Constants.TokenType.ERC20
root.sendModal.onlyAssets = true
root.sendModal.open()
}
onReceiveRequested: (symbol) => root.launchShareAddressModal()
onSwitchToCommunityRequested: (communityId) => Global.switchToCommunity(communityId)
onManageTokensRequested: Global.changeAppSectionBySectionType(Constants.appSection.profile, Constants.settingsSubsection.wallet)
}
}
CollectiblesView {
collectiblesModel: RootStore.collectiblesStore.ownedCollectibles
onCollectibleClicked: {
RootStore.collectiblesStore.getDetailedCollectible(chainId, contractAddress, tokenId)
RootStore.setCurrentViewedHolding(uid, Constants.TokenType.ERC721)
stack.currentIndex = 1
Component {
id: collectiblesView
CollectiblesView {
collectiblesModel: RootStore.collectiblesStore.ownedCollectibles
onCollectibleClicked: {
RootStore.collectiblesStore.getDetailedCollectible(chainId, contractAddress, tokenId)
RootStore.setCurrentViewedHolding(uid, Constants.TokenType.ERC721)
stack.currentIndex = 1
}
}
}
HistoryView {
Component {
id: historyView
overview: RootStore.overview
showAllAccounts: root.showAllAccounts
sendModal: root.sendModal
onLaunchTransactionDetail: function (entry, entryIndex) {
transactionDetailView.transactionIndex = entryIndex
transactionDetailView.transaction = entry
stack.currentIndex = 3
HistoryView {
overview: RootStore.overview
showAllAccounts: root.showAllAccounts
sendModal: root.sendModal
onLaunchTransactionDetail: function (entry, entryIndex) {
transactionDetailView.transactionIndex = entryIndex
transactionDetailView.transaction = entry
stack.currentIndex = 3
}
}
}
}

View File

@ -1,11 +1,6 @@
import QtQuick 2.13
import QtQuick.Controls 2.13
import QtQuick 2.15
import StatusQ.Popups 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Components 0.1
import StatusQ.Core 0.1
import StatusQ.Controls 0.1
import utils 1.0
@ -15,15 +10,18 @@ TokenDelegate {
title: Constants.dummyText
subTitle: Constants.dummyText
asset.name: Constants.dummyText
currencyBalance.text: Constants.dummyText
currencyBalance.loading: true
change24HourPercentage.text: Constants.dummyText
change24Hour.text: Constants.dummyText
localeCurrencyBalance.text: Constants.dummyText
change24HourPercentage.loading: true
currencyPrice.text: Constants.dummyText
currencyPrice.loading: true
statusListItemSubTitle.loading: true
statusListItemTitle.loading: true
statusListItemIcon.loading: true
change24HourPercentage.loading: true
change24Hour.loading: true
localeCurrencyBalance.loading: true
textColor: Theme.palette.baseColor1
enabled: false
}

View File

@ -1,20 +1,23 @@
import QtQuick 2.13
import QtQuick.Controls 2.13
import QtQuick 2.15
import QtQuick.Controls 2.15
import StatusQ.Popups 0.1
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 alias localeCurrencyBalance: localeCurrencyBalance
property alias change24Hour: change24HourText
// expected roles: name, symbol, enabledNetworkBalance, enabledNetworkCurrencyBalance, currencyPrice, changePct24hour, communityId, communityName, communityImage
property alias currencyBalance: currencyBalance
property alias change24HourPercentage: change24HourPercentageText
property alias currencyPrice: currencyPrice
property string currentCurrencySymbol
property string textColor: {
@ -33,12 +36,26 @@ StatusListItem {
property string errorTooltipText_1
property string errorTooltipText_2
readonly property bool isCommunityToken: !!modelData && !!modelData.communityId
readonly property string symbolUrl: !!modelData && modelData.symbol ? Constants.tokenIcon(modelData.symbol, false) : ""
readonly property string upDownTriangle: {
if (!modelData)
return ""
if (modelData.changePct24hour < 0)
return "▾"
if (modelData.changePct24hour > 0)
return "▴"
return ""
}
signal switchToCommunityRequested(string communityId)
title: modelData ? modelData.name : ""
subTitle: LocaleUtils.currencyAmountToLocaleString(modelData.enabledNetworkBalance)
asset.name: symbolUrl
asset.isImage: true
asset.width: 32
asset.height: 32
errorIcon.tooltip.maxWidth: 300
statusListItemTitleIcons.sourceComponent: StatusFlatRoundButton {
@ -55,7 +72,7 @@ StatusListItem {
components: [
Column {
id: valueColumn
anchors.verticalCenter: parent.verticalCenter
StatusFlatRoundButton {
id: errorIcon
width: 14
@ -69,32 +86,49 @@ StatusListItem {
visible: !!tooltip.text
}
StatusTextWithLoadingState {
id: localeCurrencyBalance
id: currencyBalance
anchors.right: parent.right
font.pixelSize: 15
text: modelData ? LocaleUtils.currencyAmountToLocaleString(modelData.enabledNetworkCurrencyBalance) : ""
visible: !errorIcon.visible
visible: !errorIcon.visible && !root.isCommunityToken
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: 8
visible: !errorIcon.visible
anchors.right: parent.right
spacing: 6
visible: !errorIcon.visible && !root.isCommunityToken
StatusTextWithLoadingState {
id: change24HourText
font.pixelSize: 15
id: change24HourPercentageText
anchors.verticalCenter: parent.verticalCenter
customColor: root.textColor
text: modelData ? LocaleUtils.currencyAmountToLocaleString(modelData.currencyPrice) : ""
font.pixelSize: 13
text: modelData && modelData.changePct24hour !== undefined ? "%1 %2%".arg(root.upDownTriangle).arg(LocaleUtils.numberToLocaleString(modelData.changePct24hour, 2))
: "---"
}
Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: 1
height: change24HourText.implicitHeight
height: 12
color: Theme.palette.directColor9
}
StatusTextWithLoadingState {
id: change24HourPercentageText
font.pixelSize: 15
id: currencyPrice
anchors.verticalCenter: parent.verticalCenter
customColor: root.textColor
text: modelData && modelData.changePct24hour !== "" ? "%1%".arg(LocaleUtils.numberToLocaleString(modelData.changePct24hour, 2)) : "---"
font.pixelSize: 13
text: modelData ? LocaleUtils.currencyAmountToLocaleString(modelData.currencyPrice) : ""
}
}
ManageTokensCommunityTag {
anchors.right: parent.right
text: modelData.communityName
imageSrc: modelData.communityImage
visible: root.isCommunityToken
StatusToolTip {
text: qsTr("This token was minted by the %1 community").arg(modelData.communityName)
visible: parent.hovered
}
TapHandler {
acceptedButtons: Qt.LeftButton
onSingleTapped: root.switchToCommunityRequested(modelData.communityId)
}
}
}
@ -102,7 +136,7 @@ StatusListItem {
states: [
State {
name: "unkownToken"
name: "unknownToken"
when: !root.symbolUrl
PropertyChanges {
target: root.asset
@ -111,6 +145,5 @@ StatusListItem {
name: !!modelData && modelData.symbol ? modelData.symbol : ""
}
}
]
}

View File

@ -15,7 +15,7 @@ QtObject {
readonly property bool marketValuesCache: walletSectionAssets.hasMarketValuesCache
readonly property var blockchainNetworksDown: !!networkConnectionModule.blockchainNetworkConnection.chainIds ? networkConnectionModule.blockchainNetworkConnection.chainIds.split(";") : []
readonly property bool atleastOneBlockchainNetworkAvailable: blockchainNetworksDown.length < networksModule.all.count
readonly property bool atleastOneBlockchainNetworkAvailable: blockchainNetworksDown.length < networksModule.all.count
readonly property bool sendBuyBridgeEnabled: localAppSettings.testEnvironment || (isOnline &&
(!networkConnectionModule.blockchainNetworkConnection.completelyDown && atleastOneBlockchainNetworkAvailable) &&
@ -30,17 +30,17 @@ QtObject {
networkConnectionModule.marketValuesNetworkConnection.completelyDown ?
qsTr("Requires CryptoCompare or CoinGecko, both of which are currently unavailable"): ""
readonly property bool notOnlineWithNoCache: !isOnline && !walletSectionAssets.hasBalanceCache && !walletSectionAssets.hasMarketValuesCache
readonly property bool notOnlineWithNoCache: !isOnline && !balanceCache && !marketValuesCache
readonly property string notOnlineWithNoCacheText: qsTr("Internet connection lost. Data could not be retrieved.")
readonly property bool noBlockchainConnectionAndNoCache: networkConnectionModule.blockchainNetworkConnection.completelyDown && !walletSectionAssets.hasBalanceCache
readonly property bool noBlockchainConnectionAndNoCache: networkConnectionModule.blockchainNetworkConnection.completelyDown && !balanceCache
readonly property string noBlockchainConnectionAndNoCacheText: qsTr("Token balances are fetched from Pocket Network (POKT) and Infura which are both curently unavailable")
readonly property bool noMarketConnectionAndNoCache: networkConnectionModule.marketValuesNetworkConnection.completelyDown && !walletSectionAssets.hasMarketValuesCache
readonly property bool noMarketConnectionAndNoCache: networkConnectionModule.marketValuesNetworkConnection.completelyDown && !marketValuesCache
readonly property string noMarketConnectionAndNoCacheText: qsTr("Market values are fetched from CryptoCompare and CoinGecko which are both currently unavailable")
readonly property bool noBlockchainAndMarketConnectionAndNoCache: noBlockchainConnectionAndNoCache && noMarketConnectionAndNoCache
readonly property string noBlockchainAndMarketConnectionAndNoCacheText: qsTr("Market values and token balances use CryptoCompare/CoinGecko and POKT/Infura which are all currently unavailable.")
readonly property string noBlockchainAndMarketConnectionAndNoCacheText: qsTr("Market values and token balances use CryptoCompare/CoinGecko and POKT/Infura which are all currently unavailable.")
readonly property bool accountBalanceNotAvailable: notOnlineWithNoCache || noBlockchainConnectionAndNoCache || noMarketConnectionAndNoCache
readonly property string accountBalanceNotAvailableText: !isOnline ? notOnlineWithNoCacheText :
@ -48,15 +48,15 @@ QtObject {
networkConnectionModule.blockchainNetworkConnection.completelyDown ? noBlockchainConnectionAndNoCacheText :
networkConnectionModule.marketValuesNetworkConnection.completelyDown ? noBlockchainAndMarketConnectionAndNoCacheText : ""
readonly property bool noTokenBalanceAvailable: networkConnectionStore.notOnlineWithNoCache || networkConnectionStore.noBlockchainConnectionAndNoCache
readonly property bool noTokenBalanceAvailable: notOnlineWithNoCache || noBlockchainConnectionAndNoCache
readonly property bool ensNetworkAvailable: !blockchainNetworksDown.includes(profileSectionModule.ensUsernamesModule.chainId.toString())
readonly property string ensNetworkUnavailableText: qsTr("Requires POKT/Infura for %1, which is currently unavailable").arg( networksModule.all.getNetworkFullName(profileSectionModule.ensUsernamesModule.chainId))
readonly property string ensNetworkUnavailableText: qsTr("Requires POKT/Infura for %1, which is currently unavailable").arg(networksModule.all.getNetworkFullName(profileSectionModule.ensUsernamesModule.chainId))
readonly property bool stickersNetworkAvailable: !blockchainNetworksDown.includes(stickersModule.getChainIdForStickers().toString())
readonly property string stickersNetworkUnavailableText: qsTr("Requires POKT/Infura for %1, which is currently unavailable").arg( networksModule.all.getNetworkFullName(stickersModule.getChainIdForStickers()))
readonly property string stickersNetworkUnavailableText: qsTr("Requires POKT/Infura for %1, which is currently unavailable").arg(networksModule.all.getNetworkFullName(stickersModule.getChainIdForStickers()))
function getBlockchainNetworkDownTextForToken(balances) {
if(!!balances && !networkConnectionModule.blockchainNetworkConnection.completelyDown && !networkConnectionStore.notOnlineWithNoCache) {
if(!!balances && !networkConnectionModule.blockchainNetworkConnection.completelyDown && !notOnlineWithNoCache) {
let chainIdsDown = []
for (var i =0; i<balances.count; i++) {
let chainId = balances.rowData(i, "chainId")
@ -73,8 +73,8 @@ QtObject {
}
function getMarketNetworkDownText() {
if(networkConnectionStore.notOnlineWithNoCache)
return networkConnectionStore.notOnlineWithNoCacheText
if(notOnlineWithNoCache)
return notOnlineWithNoCacheText
else if(noBlockchainAndMarketConnectionAndNoCache)
return noBlockchainAndMarketConnectionAndNoCacheText
else if(noMarketConnectionAndNoCache)
@ -87,7 +87,7 @@ QtObject {
let jointChainIdString = ""
for (const chain of chainIdsDown) {
jointChainIdString = (!!jointChainIdString) ? jointChainIdString + " & " : jointChainIdString
jointChainIdString += networksModule.all.getNetworkFullName(parseInt(chain))
jointChainIdString += networksModule.all.getNetworkFullName(parseInt(chain))
}
return jointChainIdString
}

View File

@ -1,88 +1,340 @@
import QtQuick 2.13
import QtQuick.Controls 2.14
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import StatusQ.Popups 0.1
import StatusQ.Popups.Dialog 0.1
import StatusQ.Models 0.1
import StatusQ.Internal 0.1
import SortFilterProxyModel 0.2
import utils 1.0
import "../stores"
import shared.stores 1.0
import shared.controls 1.0
import shared.popups 1.0
Item {
StatusScrollView {
id: root
property var assets
// expected roles: name, symbol, enabledNetworkBalance, enabledNetworkCurrencyBalance, currencyPrice, changePct24hour, communityId, communityName, communityImage
required property var assets
property var networkConnectionStore
property var overview
property bool assetDetailsLaunched: false
signal assetClicked(var token)
signal sendRequested(string symbol)
signal receiveRequested(string symbol)
signal switchToCommunityRequested(string communityId)
signal manageTokensRequested()
contentWidth: availableWidth
QtObject {
id: d
property int selectedAssetIndex: -1
}
height: assetListView.height
readonly property bool isCustomView: d.controller.hasSettings // TODO add respect other predefined orders (#12517)
StatusListView {
id: assetListView
objectName: "assetViewStatusListView"
anchors.fill: parent
model: !!assets ? assets : null
reuseItems: true
delegate: delegateLoader
}
function symbolIsVisible(symbol, balance) {
if (symbol === "ETH") // always visible
return true
if (!d.controller.filterAcceptsSymbol(symbol)) // explicitely hidden
return false
if (symbol === "SNT" || symbol === "DAI") // visible by default
return true
return !!balance && !!balance.amount // visible with non-zero balance
}
Component {
id: delegateLoader
Loader {
property var modelData: model
property int index: index
width: ListView.view.width
sourceComponent: loading ? loadingTokenDelegate: tokenDelegate
readonly property var regularAssetsModel: SortFilterProxyModel {
sourceModel: root.assets
filters: [
ExpressionFilter {
expression: {
d.controller.settingsDirty
return d.symbolIsVisible(model.symbol, model.enabledNetworkBalance) && !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.enabledNetworkBalance) && !!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"
}
function hideAllCommunityTokens(communityId) {
const tokenSymbols = ModelUtils.getAll(communityAssetsModel, "symbol", "communityId", communityId)
d.controller.settingsHideCommunityTokens(communityId, tokenSymbols)
}
}
Component {
id: loadingTokenDelegate
LoadingTokenDelegate {
objectName: "AssetView_LoadingTokenDelegate_" + index
}
}
ColumnLayout {
width: root.availableWidth
spacing: 0
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)
StatusListView {
Layout.fillWidth: true
Layout.preferredHeight: contentHeight
interactive: false
objectName: "assetViewStatusListView"
model: d.regularAssetsModel
delegate: delegateLoader
}
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
StatusBaseText {
text: qsTr("Community assets")
color: Theme.palette.baseColor1
}
errorMode: !!networkConnectionStore ? networkConnectionStore.noBlockchainConnectionAndNoCache && !networkConnectionStore.noMarketConnectionAndNoCache : false
errorIcon.tooltip.text: !!networkConnectionStore ? networkConnectionStore.noBlockchainConnectionAndNoCacheText : ""
onClicked: {
RootStore.getHistoricalDataForToken(modelData.symbol, RootStore.currencyStore.currentCurrency)
d.selectedAssetIndex = index
assetClicked(modelData)
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.onCompleted: {
// on Model reset if the detail view is shown, update the data in background.
if(root.assetDetailsLaunched && index === d.selectedAssetIndex)
assetClicked(modelData)
}
StatusListView {
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 index: index
width: ListView.view.width
sourceComponent: model.loading ? loadingTokenDelegate: tokenDelegate
}
}
Component {
id: loadingTokenDelegate
LoadingTokenDelegate {
objectName: "AssetView_LoadingTokenDelegate_" + index
}
}
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)
}
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 = index
assetClicked(modelData)
} 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 && index === d.selectedAssetIndex)
assetClicked(modelData)
}
}
}
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(`<a style="text-decoration:none" href="#${Constants.appSection.profile}/${Constants.settingsSubsection.wallet}">` + qsTr("Settings", "Go to Settings") + "</a>"),
"",
"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(`<a style="text-decoration:none" href="#${Constants.appSection.profile}/${Constants.settingsSubsection.wallet}">` + qsTr("Settings", "Go to Settings") + "</a>"),
"",
"checkmark-circle",
false,
Constants.ephemeralNotificationType.success,
""
)
}
}
}
}