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:
parent
8e0db2e666
commit
3b60506460
|
@ -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
|
|
@ -44,6 +44,11 @@ ListModel {
|
||||||
symbol: "EUR",
|
symbol: "EUR",
|
||||||
displayDecimals: 2
|
displayDecimals: 2
|
||||||
},
|
},
|
||||||
|
currencyPrice: {
|
||||||
|
amount: 10.37,
|
||||||
|
symbol: "EUR",
|
||||||
|
displayDecimals: 2
|
||||||
|
},
|
||||||
communityId: "ddls",
|
communityId: "ddls",
|
||||||
communityName: "Doodles",
|
communityName: "Doodles",
|
||||||
communityImage: ModelsData.collectibles.doodles // FIXME backend
|
communityImage: ModelsData.collectibles.doodles // FIXME backend
|
||||||
|
@ -166,6 +171,11 @@ ListModel {
|
||||||
symbol: "EUR",
|
symbol: "EUR",
|
||||||
displayDecimals: 2
|
displayDecimals: 2
|
||||||
},
|
},
|
||||||
|
currencyPrice: {
|
||||||
|
amount: 1480.113406237,
|
||||||
|
symbol: "EUR",
|
||||||
|
displayDecimals: 2
|
||||||
|
},
|
||||||
changePct24hour: -3.51,
|
changePct24hour: -3.51,
|
||||||
communityId: "",
|
communityId: "",
|
||||||
communityName: "",
|
communityName: "",
|
||||||
|
@ -185,15 +195,21 @@ ListModel {
|
||||||
enabledNetworkBalance: ({
|
enabledNetworkBalance: ({
|
||||||
displayDecimals: true,
|
displayDecimals: true,
|
||||||
stripTrailingZeroes: true,
|
stripTrailingZeroes: true,
|
||||||
amount: 324343.3,
|
amount: 0,
|
||||||
symbol: "SNT"
|
symbol: "SNT"
|
||||||
}),
|
}),
|
||||||
enabledNetworkCurrencyBalance: ({
|
enabledNetworkCurrencyBalance: ({
|
||||||
displayDecimals: 4,
|
displayDecimals: 4,
|
||||||
stripTrailingZeroes: true,
|
stripTrailingZeroes: true,
|
||||||
amount: 2.333321323400,
|
amount: 0,
|
||||||
symbol: "EUR"
|
symbol: "EUR"
|
||||||
}),
|
}),
|
||||||
|
currencyPrice: {
|
||||||
|
amount: 1.40627,
|
||||||
|
symbol: "EUR",
|
||||||
|
displayDecimals: 2
|
||||||
|
},
|
||||||
|
changePct24hour: 1.3,
|
||||||
symbol: "SNT",
|
symbol: "SNT",
|
||||||
name: "Status",
|
name: "Status",
|
||||||
communityId: "",
|
communityId: "",
|
||||||
|
|
|
@ -2,4 +2,5 @@ import QtQuick 2.15
|
||||||
|
|
||||||
QtObject {
|
QtObject {
|
||||||
property var blockchainNetworksDown: []
|
property var blockchainNetworksDown: []
|
||||||
|
property bool sendBuyBridgeEnabled: true
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ QtObject {
|
||||||
property var currentCurrency
|
property var currentCurrency
|
||||||
property bool neverAskAboutUnfurlingAgain: false
|
property bool neverAskAboutUnfurlingAgain: false
|
||||||
|
|
||||||
property var currencyStore
|
property var currencyStore: CurrenciesStore {}
|
||||||
property var history
|
property var history
|
||||||
|
|
||||||
property var getNetworkIcon
|
property var getNetworkIcon
|
||||||
|
@ -37,4 +37,8 @@ QtObject {
|
||||||
console.log("STUB: setNeverAskAboutUnfurlingAgain:", value)
|
console.log("STUB: setNeverAskAboutUnfurlingAgain:", value)
|
||||||
neverAskAboutUnfurlingAgain = value
|
neverAskAboutUnfurlingAgain = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getHistoricalDataForToken(symbol, currency) {
|
||||||
|
console.log("STUB: getHistoricalDataForToken:", symbol, currency)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,11 @@ public:
|
||||||
Q_INVOKABLE QVariant get(QAbstractItemModel *model, int row,
|
Q_INVOKABLE QVariant get(QAbstractItemModel *model, int row,
|
||||||
const QString &roleName) const;
|
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;
|
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;
|
///< performs a strict check whether @lhs and @rhs arrays (QList<T>) contain the same elements;
|
||||||
|
|
|
@ -116,6 +116,7 @@ Row {
|
||||||
id: textLayout
|
id: textLayout
|
||||||
width: !iconOrImage.active ? parent.width :
|
width: !iconOrImage.active ? parent.width :
|
||||||
parent.width - iconOrImage.width - parent.spacing
|
parent.width - iconOrImage.width - parent.spacing
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
Row {
|
Row {
|
||||||
id: headerTitleRow
|
id: headerTitleRow
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
|
|
@ -60,6 +60,32 @@ QVariant ModelUtilsInternal::get(QAbstractItemModel *model,
|
||||||
return {};
|
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,
|
bool ModelUtilsInternal::contains(QAbstractItemModel* model,
|
||||||
const QString& roleName,
|
const QString& roleName,
|
||||||
const QVariant& value,
|
const QVariant& value,
|
||||||
|
|
|
@ -110,7 +110,7 @@ QHash<int, QByteArray> RolesRenamingModel::roleNames() const
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (renameMap.size()) {
|
if (!renameMap.isEmpty()) {
|
||||||
qWarning().nospace()
|
qWarning().nospace()
|
||||||
<< "RolesRenamingModel: specified source roles not found: "
|
<< "RolesRenamingModel: specified source roles not found: "
|
||||||
<< renameMap.keys() << "!";
|
<< renameMap.keys() << "!";
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
#include "managetokenscontroller.h"
|
#include "managetokenscontroller.h"
|
||||||
|
|
||||||
#include <QElapsedTimer>
|
#include <QElapsedTimer>
|
||||||
|
#include <QMutableHashIterator>
|
||||||
|
|
||||||
ManageTokensController::ManageTokensController(QObject* parent)
|
ManageTokensController::ManageTokensController(QObject* parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
|
@ -112,18 +113,24 @@ void ManageTokensController::showHideGroup(const QString& groupId, bool flag)
|
||||||
rebuildCommunityTokenGroupsModel();
|
rebuildCommunityTokenGroupsModel();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ManageTokensController::saveSettings()
|
void ManageTokensController::saveSettings(bool reuseCurrent)
|
||||||
{
|
{
|
||||||
Q_ASSERT(!m_settingsKey.isEmpty());
|
Q_ASSERT(!m_settingsKey.isEmpty());
|
||||||
|
|
||||||
|
setSettingsDirty(true);
|
||||||
|
|
||||||
if (m_arrangeByCommunity)
|
if (m_arrangeByCommunity)
|
||||||
m_communityTokensModel->applySort();
|
m_communityTokensModel->applySort();
|
||||||
|
|
||||||
// gather the data to save
|
// gather the data to save
|
||||||
SerializedTokenData result;
|
SerializedTokenData result;
|
||||||
for (auto model: {m_regularTokensModel, m_communityTokensModel})
|
if (reuseCurrent) {
|
||||||
result.insert(model->save());
|
result = m_settingsData;
|
||||||
result.insert(m_hiddenTokensModel->save(false));
|
} else {
|
||||||
|
for(auto model : {m_regularTokensModel, m_communityTokensModel})
|
||||||
|
result.insert(model->save());
|
||||||
|
result.insert(m_hiddenTokensModel->save(false));
|
||||||
|
}
|
||||||
|
|
||||||
// save to QSettings
|
// save to QSettings
|
||||||
m_settings.beginGroup(settingsGroupName());
|
m_settings.beginGroup(settingsGroupName());
|
||||||
|
@ -144,6 +151,8 @@ void ManageTokensController::saveSettings()
|
||||||
// unset dirty
|
// unset dirty
|
||||||
for (auto model: m_allModels)
|
for (auto model: m_allModels)
|
||||||
model->setDirty(false);
|
model->setDirty(false);
|
||||||
|
|
||||||
|
setSettingsDirty(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ManageTokensController::clearSettings()
|
void ManageTokensController::clearSettings()
|
||||||
|
@ -182,6 +191,13 @@ void ManageTokensController::loadSettings()
|
||||||
m_settings.endGroup();
|
m_settings.endGroup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ManageTokensController::setSettingsDirty(bool dirty)
|
||||||
|
{
|
||||||
|
if (m_settingsDirty == dirty) return;
|
||||||
|
m_settingsDirty = dirty;
|
||||||
|
emit settingsDirtyChanged(m_settingsDirty);
|
||||||
|
}
|
||||||
|
|
||||||
void ManageTokensController::revert()
|
void ManageTokensController::revert()
|
||||||
{
|
{
|
||||||
parseSourceModel();
|
parseSourceModel();
|
||||||
|
@ -199,6 +215,40 @@ bool ManageTokensController::hasSettings() const
|
||||||
return !groups.isEmpty() && groups.contains(settingsGroupName());
|
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
|
bool ManageTokensController::lessThan(const QString& lhsSymbol, const QString& rhsSymbol) const
|
||||||
{
|
{
|
||||||
int leftPos, rightPos;
|
int leftPos, rightPos;
|
||||||
|
@ -216,7 +266,7 @@ bool ManageTokensController::lessThan(const QString& lhsSymbol, const QString& r
|
||||||
|
|
||||||
bool ManageTokensController::filterAcceptsSymbol(const QString& symbol) const
|
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;
|
return visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,9 +14,9 @@ class ManageTokensController : public QObject, public QQmlParserStatus
|
||||||
Q_INTERFACES(QQmlParserStatus)
|
Q_INTERFACES(QQmlParserStatus)
|
||||||
|
|
||||||
// input properties
|
// 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(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
|
// output properties
|
||||||
Q_PROPERTY(QAbstractItemModel* regularTokensModel READ regularTokensModel CONSTANT FINAL)
|
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* communityTokenGroupsModel READ communityTokenGroupsModel CONSTANT FINAL)
|
||||||
Q_PROPERTY(QAbstractItemModel* hiddenTokensModel READ hiddenTokensModel CONSTANT FINAL)
|
Q_PROPERTY(QAbstractItemModel* hiddenTokensModel READ hiddenTokensModel CONSTANT FINAL)
|
||||||
Q_PROPERTY(bool dirty READ dirty NOTIFY dirtyChanged FINAL)
|
Q_PROPERTY(bool dirty READ dirty NOTIFY dirtyChanged FINAL)
|
||||||
|
Q_PROPERTY(bool settingsDirty READ settingsDirty NOTIFY settingsDirtyChanged FINAL)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit ManageTokensController(QObject* parent = nullptr);
|
explicit ManageTokensController(QObject* parent = nullptr);
|
||||||
|
@ -33,11 +34,15 @@ public:
|
||||||
Q_INVOKABLE void showHideCommunityToken(int row, bool flag);
|
Q_INVOKABLE void showHideCommunityToken(int row, bool flag);
|
||||||
Q_INVOKABLE void showHideGroup(const QString& groupId, 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 clearSettings();
|
||||||
Q_INVOKABLE void revert();
|
Q_INVOKABLE void revert();
|
||||||
Q_INVOKABLE bool hasSettings() const;
|
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 lessThan(const QString& lhsSymbol, const QString& rhsSymbol) const;
|
||||||
Q_INVOKABLE bool filterAcceptsSymbol(const QString& symbol) const;
|
Q_INVOKABLE bool filterAcceptsSymbol(const QString& symbol) const;
|
||||||
|
|
||||||
|
@ -50,6 +55,7 @@ signals:
|
||||||
void dirtyChanged();
|
void dirtyChanged();
|
||||||
void arrangeByCommunityChanged();
|
void arrangeByCommunityChanged();
|
||||||
void settingsKeyChanged();
|
void settingsKeyChanged();
|
||||||
|
void settingsDirtyChanged(bool dirty);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QAbstractItemModel* m_sourceModel{nullptr};
|
QAbstractItemModel* m_sourceModel{nullptr};
|
||||||
|
@ -60,16 +66,16 @@ private:
|
||||||
void addItem(int index);
|
void addItem(int index);
|
||||||
|
|
||||||
ManageTokensModel* m_regularTokensModel{nullptr};
|
ManageTokensModel* m_regularTokensModel{nullptr};
|
||||||
QAbstractItemModel* regularTokensModel() const { return m_regularTokensModel; };
|
QAbstractItemModel* regularTokensModel() const { return m_regularTokensModel; }
|
||||||
|
|
||||||
ManageTokensModel* m_communityTokensModel{nullptr};
|
ManageTokensModel* m_communityTokensModel{nullptr};
|
||||||
QAbstractItemModel* communityTokensModel() const { return m_communityTokensModel; };
|
QAbstractItemModel* communityTokensModel() const { return m_communityTokensModel; }
|
||||||
|
|
||||||
ManageTokensModel* m_communityTokenGroupsModel{nullptr};
|
ManageTokensModel* m_communityTokenGroupsModel{nullptr};
|
||||||
QAbstractItemModel* communityTokenGroupsModel() const { return m_communityTokenGroupsModel; };
|
QAbstractItemModel* communityTokenGroupsModel() const { return m_communityTokenGroupsModel; }
|
||||||
|
|
||||||
ManageTokensModel* m_hiddenTokensModel{nullptr};
|
ManageTokensModel* m_hiddenTokensModel{nullptr};
|
||||||
QAbstractItemModel* hiddenTokensModel() const { return m_hiddenTokensModel; };
|
QAbstractItemModel* hiddenTokensModel() const { return m_hiddenTokensModel; }
|
||||||
|
|
||||||
bool dirty() const;
|
bool dirty() const;
|
||||||
|
|
||||||
|
@ -88,8 +94,11 @@ private:
|
||||||
QString settingsGroupName() const;
|
QString settingsGroupName() const;
|
||||||
void setSettingsKey(const QString& newSettingsKey);
|
void setSettingsKey(const QString& newSettingsKey);
|
||||||
QSettings m_settings;
|
QSettings m_settings;
|
||||||
void loadSettings();
|
|
||||||
SerializedTokenData m_settingsData; // symbol -> {sortOrder, visible, groupId}
|
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};
|
bool m_modelConnectionsInitialized{false};
|
||||||
};
|
};
|
||||||
|
|
|
@ -63,7 +63,7 @@ SettingsContentBase {
|
||||||
onSaveChangesClicked: {
|
onSaveChangesClicked: {
|
||||||
manageTokensView.saveChanges()
|
manageTokensView.saveChanges()
|
||||||
Global.displayToastMessage(
|
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>"),
|
.arg(`<a style="text-decoration:none" href="#${Constants.appSection.wallet}">` + qsTr("Wallet", "Go to Wallet") + "</a>"),
|
||||||
"",
|
"",
|
||||||
"checkmark-circle",
|
"checkmark-circle",
|
||||||
|
|
|
@ -23,7 +23,7 @@ ColumnLayout {
|
||||||
return false
|
return false
|
||||||
if (tabBar.currentIndex > d.collectiblesTabIndex)
|
if (tabBar.currentIndex > d.collectiblesTabIndex)
|
||||||
return false
|
return false
|
||||||
if (tabBar.currentIndex === d.collectiblesTabIndex && baseCollectiblesModel.isFetching)
|
if (tabBar.currentIndex === d.collectiblesTabIndex && baseWalletCollectiblesModel.isFetching)
|
||||||
return false
|
return false
|
||||||
return loader.item && loader.item.dirty
|
return loader.item && loader.item.dirty
|
||||||
}
|
}
|
||||||
|
@ -51,14 +51,14 @@ ColumnLayout {
|
||||||
if (tabBar.currentIndex !== collectiblesTabIndex)
|
if (tabBar.currentIndex !== collectiblesTabIndex)
|
||||||
return
|
return
|
||||||
// If there is no more items to load or we're already fetching, 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
|
return
|
||||||
root.baseCollectiblesModel.loadMore()
|
root.baseWalletCollectiblesModel.loadMore()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: root.baseCollectiblesModel
|
target: root.baseWalletCollectiblesModel
|
||||||
function onHasMoreChanged() {
|
function onHasMoreChanged() {
|
||||||
d.checkLoadMoreCollectibles()
|
d.checkLoadMoreCollectibles()
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,8 @@ Item {
|
||||||
function resetView() {
|
function resetView() {
|
||||||
stack.currentIndex = 0
|
stack.currentIndex = 0
|
||||||
root.currentTabIndex = 0
|
root.currentTabIndex = 0
|
||||||
historyView.resetView()
|
if (walletTabBar.currentIndex === 2)
|
||||||
|
mainViewLoader.item.resetView()
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetStack() {
|
function resetStack() {
|
||||||
|
@ -94,7 +95,6 @@ Item {
|
||||||
StatusTabBar {
|
StatusTabBar {
|
||||||
id: walletTabBar
|
id: walletTabBar
|
||||||
objectName: "rightSideWalletTabBar"
|
objectName: "rightSideWalletTabBar"
|
||||||
horizontalPadding: Style.current.padding
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.topMargin: Style.current.padding
|
Layout.topMargin: Style.current.padding
|
||||||
|
|
||||||
|
@ -116,41 +116,67 @@ Item {
|
||||||
RootStore.setCurrentViewedHoldingType(walletTabBar.currentIndex === 1 ? Constants.TokenType.ERC721 : Constants.TokenType.ERC20)
|
RootStore.setCurrentViewedHoldingType(walletTabBar.currentIndex === 1 ? Constants.TokenType.ERC721 : Constants.TokenType.ERC20)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
StackLayout {
|
Loader {
|
||||||
|
id: mainViewLoader
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
Layout.topMargin: Style.current.padding
|
Layout.topMargin: Style.current.padding
|
||||||
Layout.bottomMargin: 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 {
|
Component {
|
||||||
assets: RootStore.assets
|
id: assetsView
|
||||||
networkConnectionStore: root.networkConnectionStore
|
AssetsView {
|
||||||
assetDetailsLaunched: stack.currentIndex === 2
|
assets: RootStore.assets
|
||||||
onAssetClicked: {
|
overview: RootStore.overview
|
||||||
assetDetailView.token = token
|
networkConnectionStore: root.networkConnectionStore
|
||||||
RootStore.setCurrentViewedHolding(token.symbol, Constants.TokenType.ERC20)
|
assetDetailsLaunched: stack.currentIndex === 2
|
||||||
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 {
|
Component {
|
||||||
collectiblesModel: RootStore.collectiblesStore.ownedCollectibles
|
id: collectiblesView
|
||||||
onCollectibleClicked: {
|
CollectiblesView {
|
||||||
RootStore.collectiblesStore.getDetailedCollectible(chainId, contractAddress, tokenId)
|
collectiblesModel: RootStore.collectiblesStore.ownedCollectibles
|
||||||
RootStore.setCurrentViewedHolding(uid, Constants.TokenType.ERC721)
|
onCollectibleClicked: {
|
||||||
stack.currentIndex = 1
|
RootStore.collectiblesStore.getDetailedCollectible(chainId, contractAddress, tokenId)
|
||||||
|
RootStore.setCurrentViewedHolding(uid, Constants.TokenType.ERC721)
|
||||||
|
stack.currentIndex = 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
HistoryView {
|
Component {
|
||||||
id: historyView
|
id: historyView
|
||||||
overview: RootStore.overview
|
HistoryView {
|
||||||
showAllAccounts: root.showAllAccounts
|
overview: RootStore.overview
|
||||||
sendModal: root.sendModal
|
showAllAccounts: root.showAllAccounts
|
||||||
onLaunchTransactionDetail: function (entry, entryIndex) {
|
sendModal: root.sendModal
|
||||||
transactionDetailView.transactionIndex = entryIndex
|
onLaunchTransactionDetail: function (entry, entryIndex) {
|
||||||
transactionDetailView.transaction = entry
|
transactionDetailView.transactionIndex = entryIndex
|
||||||
|
transactionDetailView.transaction = entry
|
||||||
stack.currentIndex = 3
|
stack.currentIndex = 3
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,6 @@
|
||||||
import QtQuick 2.13
|
import QtQuick 2.15
|
||||||
import QtQuick.Controls 2.13
|
|
||||||
|
|
||||||
import StatusQ.Popups 0.1
|
|
||||||
import StatusQ.Core.Theme 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
|
import utils 1.0
|
||||||
|
|
||||||
|
@ -15,15 +10,18 @@ TokenDelegate {
|
||||||
title: Constants.dummyText
|
title: Constants.dummyText
|
||||||
subTitle: Constants.dummyText
|
subTitle: Constants.dummyText
|
||||||
asset.name: Constants.dummyText
|
asset.name: Constants.dummyText
|
||||||
|
|
||||||
|
currencyBalance.text: Constants.dummyText
|
||||||
|
currencyBalance.loading: true
|
||||||
change24HourPercentage.text: Constants.dummyText
|
change24HourPercentage.text: Constants.dummyText
|
||||||
change24Hour.text: Constants.dummyText
|
change24HourPercentage.loading: true
|
||||||
localeCurrencyBalance.text: Constants.dummyText
|
currencyPrice.text: Constants.dummyText
|
||||||
|
currencyPrice.loading: true
|
||||||
|
|
||||||
statusListItemSubTitle.loading: true
|
statusListItemSubTitle.loading: true
|
||||||
statusListItemTitle.loading: true
|
statusListItemTitle.loading: true
|
||||||
statusListItemIcon.loading: true
|
statusListItemIcon.loading: true
|
||||||
change24HourPercentage.loading: true
|
|
||||||
change24Hour.loading: true
|
|
||||||
localeCurrencyBalance.loading: true
|
|
||||||
textColor: Theme.palette.baseColor1
|
textColor: Theme.palette.baseColor1
|
||||||
enabled: false
|
enabled: false
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,23 @@
|
||||||
import QtQuick 2.13
|
import QtQuick 2.15
|
||||||
import QtQuick.Controls 2.13
|
import QtQuick.Controls 2.15
|
||||||
|
|
||||||
import StatusQ.Popups 0.1
|
|
||||||
import StatusQ.Core.Theme 0.1
|
import StatusQ.Core.Theme 0.1
|
||||||
import StatusQ.Components 0.1
|
import StatusQ.Components 0.1
|
||||||
import StatusQ.Core 0.1
|
import StatusQ.Core 0.1
|
||||||
import StatusQ.Controls 0.1
|
import StatusQ.Controls 0.1
|
||||||
|
|
||||||
|
import AppLayouts.Wallet.controls 1.0
|
||||||
|
|
||||||
import utils 1.0
|
import utils 1.0
|
||||||
|
|
||||||
StatusListItem {
|
StatusListItem {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property alias localeCurrencyBalance: localeCurrencyBalance
|
// expected roles: name, symbol, enabledNetworkBalance, enabledNetworkCurrencyBalance, currencyPrice, changePct24hour, communityId, communityName, communityImage
|
||||||
property alias change24Hour: change24HourText
|
|
||||||
|
property alias currencyBalance: currencyBalance
|
||||||
property alias change24HourPercentage: change24HourPercentageText
|
property alias change24HourPercentage: change24HourPercentageText
|
||||||
|
property alias currencyPrice: currencyPrice
|
||||||
|
|
||||||
property string currentCurrencySymbol
|
property string currentCurrencySymbol
|
||||||
property string textColor: {
|
property string textColor: {
|
||||||
|
@ -33,12 +36,26 @@ StatusListItem {
|
||||||
property string errorTooltipText_1
|
property string errorTooltipText_1
|
||||||
property string errorTooltipText_2
|
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 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 : ""
|
title: modelData ? modelData.name : ""
|
||||||
subTitle: LocaleUtils.currencyAmountToLocaleString(modelData.enabledNetworkBalance)
|
subTitle: LocaleUtils.currencyAmountToLocaleString(modelData.enabledNetworkBalance)
|
||||||
asset.name: symbolUrl
|
asset.name: symbolUrl
|
||||||
asset.isImage: true
|
asset.isImage: true
|
||||||
|
asset.width: 32
|
||||||
|
asset.height: 32
|
||||||
errorIcon.tooltip.maxWidth: 300
|
errorIcon.tooltip.maxWidth: 300
|
||||||
|
|
||||||
statusListItemTitleIcons.sourceComponent: StatusFlatRoundButton {
|
statusListItemTitleIcons.sourceComponent: StatusFlatRoundButton {
|
||||||
|
@ -55,7 +72,7 @@ StatusListItem {
|
||||||
|
|
||||||
components: [
|
components: [
|
||||||
Column {
|
Column {
|
||||||
id: valueColumn
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
StatusFlatRoundButton {
|
StatusFlatRoundButton {
|
||||||
id: errorIcon
|
id: errorIcon
|
||||||
width: 14
|
width: 14
|
||||||
|
@ -69,32 +86,49 @@ StatusListItem {
|
||||||
visible: !!tooltip.text
|
visible: !!tooltip.text
|
||||||
}
|
}
|
||||||
StatusTextWithLoadingState {
|
StatusTextWithLoadingState {
|
||||||
id: localeCurrencyBalance
|
id: currencyBalance
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
font.pixelSize: 15
|
|
||||||
text: modelData ? LocaleUtils.currencyAmountToLocaleString(modelData.enabledNetworkCurrencyBalance) : ""
|
text: modelData ? LocaleUtils.currencyAmountToLocaleString(modelData.enabledNetworkCurrencyBalance) : ""
|
||||||
visible: !errorIcon.visible
|
visible: !errorIcon.visible && !root.isCommunityToken
|
||||||
}
|
}
|
||||||
Row {
|
Row {
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.right: parent.right
|
||||||
spacing: 8
|
spacing: 6
|
||||||
visible: !errorIcon.visible
|
visible: !errorIcon.visible && !root.isCommunityToken
|
||||||
StatusTextWithLoadingState {
|
StatusTextWithLoadingState {
|
||||||
id: change24HourText
|
id: change24HourPercentageText
|
||||||
font.pixelSize: 15
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
customColor: root.textColor
|
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 {
|
Rectangle {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
width: 1
|
width: 1
|
||||||
height: change24HourText.implicitHeight
|
height: 12
|
||||||
color: Theme.palette.directColor9
|
color: Theme.palette.directColor9
|
||||||
}
|
}
|
||||||
StatusTextWithLoadingState {
|
StatusTextWithLoadingState {
|
||||||
id: change24HourPercentageText
|
id: currencyPrice
|
||||||
font.pixelSize: 15
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
customColor: root.textColor
|
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: [
|
states: [
|
||||||
State {
|
State {
|
||||||
name: "unkownToken"
|
name: "unknownToken"
|
||||||
when: !root.symbolUrl
|
when: !root.symbolUrl
|
||||||
PropertyChanges {
|
PropertyChanges {
|
||||||
target: root.asset
|
target: root.asset
|
||||||
|
@ -111,6 +145,5 @@ StatusListItem {
|
||||||
name: !!modelData && modelData.symbol ? modelData.symbol : ""
|
name: !!modelData && modelData.symbol ? modelData.symbol : ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ QtObject {
|
||||||
readonly property bool marketValuesCache: walletSectionAssets.hasMarketValuesCache
|
readonly property bool marketValuesCache: walletSectionAssets.hasMarketValuesCache
|
||||||
|
|
||||||
readonly property var blockchainNetworksDown: !!networkConnectionModule.blockchainNetworkConnection.chainIds ? networkConnectionModule.blockchainNetworkConnection.chainIds.split(";") : []
|
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 &&
|
readonly property bool sendBuyBridgeEnabled: localAppSettings.testEnvironment || (isOnline &&
|
||||||
(!networkConnectionModule.blockchainNetworkConnection.completelyDown && atleastOneBlockchainNetworkAvailable) &&
|
(!networkConnectionModule.blockchainNetworkConnection.completelyDown && atleastOneBlockchainNetworkAvailable) &&
|
||||||
|
@ -30,17 +30,17 @@ QtObject {
|
||||||
networkConnectionModule.marketValuesNetworkConnection.completelyDown ?
|
networkConnectionModule.marketValuesNetworkConnection.completelyDown ?
|
||||||
qsTr("Requires CryptoCompare or CoinGecko, both of which are currently unavailable"): ""
|
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 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 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 string noMarketConnectionAndNoCacheText: qsTr("Market values are fetched from CryptoCompare and CoinGecko which are both currently unavailable")
|
||||||
|
|
||||||
readonly property bool noBlockchainAndMarketConnectionAndNoCache: noBlockchainConnectionAndNoCache && noMarketConnectionAndNoCache
|
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 bool accountBalanceNotAvailable: notOnlineWithNoCache || noBlockchainConnectionAndNoCache || noMarketConnectionAndNoCache
|
||||||
readonly property string accountBalanceNotAvailableText: !isOnline ? notOnlineWithNoCacheText :
|
readonly property string accountBalanceNotAvailableText: !isOnline ? notOnlineWithNoCacheText :
|
||||||
|
@ -48,15 +48,15 @@ QtObject {
|
||||||
networkConnectionModule.blockchainNetworkConnection.completelyDown ? noBlockchainConnectionAndNoCacheText :
|
networkConnectionModule.blockchainNetworkConnection.completelyDown ? noBlockchainConnectionAndNoCacheText :
|
||||||
networkConnectionModule.marketValuesNetworkConnection.completelyDown ? noBlockchainAndMarketConnectionAndNoCacheText : ""
|
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 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 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) {
|
function getBlockchainNetworkDownTextForToken(balances) {
|
||||||
if(!!balances && !networkConnectionModule.blockchainNetworkConnection.completelyDown && !networkConnectionStore.notOnlineWithNoCache) {
|
if(!!balances && !networkConnectionModule.blockchainNetworkConnection.completelyDown && !notOnlineWithNoCache) {
|
||||||
let chainIdsDown = []
|
let chainIdsDown = []
|
||||||
for (var i =0; i<balances.count; i++) {
|
for (var i =0; i<balances.count; i++) {
|
||||||
let chainId = balances.rowData(i, "chainId")
|
let chainId = balances.rowData(i, "chainId")
|
||||||
|
@ -73,8 +73,8 @@ QtObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMarketNetworkDownText() {
|
function getMarketNetworkDownText() {
|
||||||
if(networkConnectionStore.notOnlineWithNoCache)
|
if(notOnlineWithNoCache)
|
||||||
return networkConnectionStore.notOnlineWithNoCacheText
|
return notOnlineWithNoCacheText
|
||||||
else if(noBlockchainAndMarketConnectionAndNoCache)
|
else if(noBlockchainAndMarketConnectionAndNoCache)
|
||||||
return noBlockchainAndMarketConnectionAndNoCacheText
|
return noBlockchainAndMarketConnectionAndNoCacheText
|
||||||
else if(noMarketConnectionAndNoCache)
|
else if(noMarketConnectionAndNoCache)
|
||||||
|
@ -87,7 +87,7 @@ QtObject {
|
||||||
let jointChainIdString = ""
|
let jointChainIdString = ""
|
||||||
for (const chain of chainIdsDown) {
|
for (const chain of chainIdsDown) {
|
||||||
jointChainIdString = (!!jointChainIdString) ? jointChainIdString + " & " : jointChainIdString
|
jointChainIdString = (!!jointChainIdString) ? jointChainIdString + " & " : jointChainIdString
|
||||||
jointChainIdString += networksModule.all.getNetworkFullName(parseInt(chain))
|
jointChainIdString += networksModule.all.getNetworkFullName(parseInt(chain))
|
||||||
}
|
}
|
||||||
return jointChainIdString
|
return jointChainIdString
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,88 +1,340 @@
|
||||||
import QtQuick 2.13
|
import QtQuick 2.15
|
||||||
import QtQuick.Controls 2.14
|
import QtQuick.Controls 2.15
|
||||||
|
import QtQuick.Layouts 1.15
|
||||||
|
|
||||||
import StatusQ.Core 0.1
|
import StatusQ.Core 0.1
|
||||||
import StatusQ.Core.Theme 0.1
|
import StatusQ.Core.Theme 0.1
|
||||||
import StatusQ.Controls 0.1
|
import StatusQ.Controls 0.1
|
||||||
import StatusQ.Components 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 SortFilterProxyModel 0.2
|
||||||
|
|
||||||
import utils 1.0
|
import utils 1.0
|
||||||
|
|
||||||
import "../stores"
|
import shared.stores 1.0
|
||||||
import shared.controls 1.0
|
import shared.controls 1.0
|
||||||
|
import shared.popups 1.0
|
||||||
|
|
||||||
Item {
|
StatusScrollView {
|
||||||
id: root
|
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 networkConnectionStore
|
||||||
|
property var overview
|
||||||
property bool assetDetailsLaunched: false
|
property bool assetDetailsLaunched: false
|
||||||
|
|
||||||
signal assetClicked(var token)
|
signal assetClicked(var token)
|
||||||
|
signal sendRequested(string symbol)
|
||||||
|
signal receiveRequested(string symbol)
|
||||||
|
signal switchToCommunityRequested(string communityId)
|
||||||
|
signal manageTokensRequested()
|
||||||
|
|
||||||
|
contentWidth: availableWidth
|
||||||
|
|
||||||
QtObject {
|
QtObject {
|
||||||
id: d
|
id: d
|
||||||
property int selectedAssetIndex: -1
|
property int selectedAssetIndex: -1
|
||||||
}
|
|
||||||
|
|
||||||
height: assetListView.height
|
readonly property bool isCustomView: d.controller.hasSettings // TODO add respect other predefined orders (#12517)
|
||||||
|
|
||||||
StatusListView {
|
function symbolIsVisible(symbol, balance) {
|
||||||
id: assetListView
|
if (symbol === "ETH") // always visible
|
||||||
objectName: "assetViewStatusListView"
|
return true
|
||||||
anchors.fill: parent
|
if (!d.controller.filterAcceptsSymbol(symbol)) // explicitely hidden
|
||||||
model: !!assets ? assets : null
|
return false
|
||||||
reuseItems: true
|
if (symbol === "SNT" || symbol === "DAI") // visible by default
|
||||||
delegate: delegateLoader
|
return true
|
||||||
}
|
return !!balance && !!balance.amount // visible with non-zero balance
|
||||||
|
}
|
||||||
|
|
||||||
Component {
|
readonly property var regularAssetsModel: SortFilterProxyModel {
|
||||||
id: delegateLoader
|
sourceModel: root.assets
|
||||||
Loader {
|
|
||||||
property var modelData: model
|
filters: [
|
||||||
property int index: index
|
ExpressionFilter {
|
||||||
width: ListView.view.width
|
expression: {
|
||||||
sourceComponent: loading ? loadingTokenDelegate: tokenDelegate
|
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 {
|
ColumnLayout {
|
||||||
id: loadingTokenDelegate
|
width: root.availableWidth
|
||||||
LoadingTokenDelegate {
|
spacing: 0
|
||||||
objectName: "AssetView_LoadingTokenDelegate_" + index
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
StatusListView {
|
||||||
id: tokenDelegate
|
Layout.fillWidth: true
|
||||||
TokenDelegate {
|
Layout.preferredHeight: contentHeight
|
||||||
objectName: "AssetView_TokenListItem_" + (!!modelData ? modelData.symbol : "")
|
interactive: false
|
||||||
readonly property string balance: !!modelData ? "%1".arg(modelData.enabledNetworkBalance.amount) : "" // Needed for the tests
|
objectName: "assetViewStatusListView"
|
||||||
errorTooltipText_1: !!modelData && !! networkConnectionStore ? networkConnectionStore.getBlockchainNetworkDownTextForToken(modelData.balances) : ""
|
model: d.regularAssetsModel
|
||||||
errorTooltipText_2: !!networkConnectionStore ? networkConnectionStore.getMarketNetworkDownText() : ""
|
delegate: delegateLoader
|
||||||
subTitle: {
|
}
|
||||||
if (!modelData) {
|
|
||||||
return ""
|
StatusDialogDivider {
|
||||||
}
|
Layout.fillWidth: true
|
||||||
if (networkConnectionStore && networkConnectionStore.noTokenBalanceAvailable) {
|
Layout.topMargin: Style.current.padding
|
||||||
return ""
|
Layout.bottomMargin: Style.current.halfPadding
|
||||||
}
|
visible: d.hasCommunityAssets
|
||||||
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
|
Item { Layout.fillWidth: true }
|
||||||
errorIcon.tooltip.text: !!networkConnectionStore ? networkConnectionStore.noBlockchainConnectionAndNoCacheText : ""
|
StatusFlatButton {
|
||||||
onClicked: {
|
Layout.preferredWidth: 32
|
||||||
RootStore.getHistoricalDataForToken(modelData.symbol, RootStore.currencyStore.currentCurrency)
|
Layout.preferredHeight: 32
|
||||||
d.selectedAssetIndex = index
|
icon.name: "info"
|
||||||
assetClicked(modelData)
|
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)
|
StatusListView {
|
||||||
assetClicked(modelData)
|
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,
|
||||||
|
""
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue