feat: Integrate the new manage assets/collectibles panels into Settings

- display a customized floating bubble to save/apply the settings
- display a toast message on Apply with an action to jump to Wallet main
page
- add home/end/pgup/pgdown keyboard shortcuts to StatusScrollView to be
able to navigate more easily in long list views
- some smaller fixes and cleanups in wallet and settings related views

Fixes https://github.com/status-im/status-desktop/issues/12762
This commit is contained in:
Lukáš Tinkl 2023-11-17 15:08:43 +01:00 committed by Lukáš Tinkl
parent 77197040a4
commit 8791d028e6
23 changed files with 266 additions and 76 deletions

View File

@ -24,8 +24,9 @@ SplitView {
StatusScrollView { // wrapped in a ScrollView on purpose; to simulate SettingsContentBase.qml
SplitView.fillWidth: true
SplitView.preferredHeight: 500
ManageTokensPanel {
SplitView.fillHeight: true
Component.onCompleted: forceActiveFocus()
ManageAssetsPanel {
id: showcasePanel
width: 500
baseModel: ctrlEmptyModel.checked ? null : assetsModel

View File

@ -26,6 +26,9 @@ SplitView {
StatusScrollView { // wrapped in a ScrollView on purpose; to simulate SettingsContentBase.qml
SplitView.fillWidth: true
SplitView.fillHeight: true
Component.onCompleted: forceActiveFocus()
ManageCollectiblesPanel {
id: showcasePanel
width: 500

View File

@ -181,6 +181,43 @@ T.ScrollView {
applyFlickableFix()
}
Keys.onPressed: {
switch (event.key) {
case Qt.Key_Home:
scrollHome()
event.accepted = true
break
case Qt.Key_End:
scrollEnd()
event.accepted = true
break
case Qt.Key_PageUp:
scrollPageUp()
event.accepted = true
break
case Qt.Key_PageDown:
scrollPageDown()
event.accepted = true
break
}
}
function scrollHome() {
flickable.contentY = 0
}
function scrollEnd() {
flickable.contentY = flickable.contentHeight - flickable.height
}
function scrollPageUp() {
root.ScrollBar.vertical.decrease()
}
function scrollPageDown() {
root.ScrollBar.vertical.increase()
}
ScrollBar.vertical: StatusScrollBar {
parent: root
x: root.mirrored ? 1 : root.width - width - 1

View File

@ -126,7 +126,7 @@ void ManageTokensController::saveSettings()
result.insert(m_hiddenTokensModel->save(false));
// save to QSettings
m_settings.beginGroup(QStringLiteral("ManageTokens-%1").arg(m_settingsKey));
m_settings.beginGroup(settingsGroupName());
m_settings.beginWriteArray(m_settingsKey);
SerializedTokenData::const_key_value_iterator it = result.constKeyValueBegin();
for (auto i = 0; it != result.constKeyValueEnd() && i < result.size(); it++, i++) {
@ -151,7 +151,7 @@ void ManageTokensController::clearSettings()
Q_ASSERT(!m_settingsKey.isEmpty());
// clear the relevant QSettings group
m_settings.beginGroup(QStringLiteral("ManageTokens-%1").arg(m_settingsKey));
m_settings.beginGroup(settingsGroupName());
m_settings.remove(QString());
m_settings.endGroup();
m_settings.sync();
@ -164,7 +164,7 @@ void ManageTokensController::loadSettings()
m_settingsData.clear();
// load from QSettings
m_settings.beginGroup(QStringLiteral("ManageTokens-%1").arg(m_settingsKey));
m_settings.beginGroup(settingsGroupName());
const auto size = m_settings.beginReadArray(m_settingsKey);
for (auto i = 0; i < size; i++) {
m_settings.setArrayIndex(i);
@ -187,6 +187,36 @@ void ManageTokensController::revert()
parseSourceModel();
}
QString ManageTokensController::settingsGroupName() const
{
return QStringLiteral("ManageTokens-%1").arg(m_settingsKey);
}
bool ManageTokensController::hasSettings() const
{
Q_ASSERT(!m_settingsKey.isEmpty());
const auto groups = m_settings.childGroups();
return !groups.isEmpty() && groups.contains(settingsGroupName());
}
bool ManageTokensController::lessThan(const QString& lhsSymbol, const QString& rhsSymbol) const
{
auto [leftPos, leftVisible, leftGroup] = m_settingsData.value(lhsSymbol, {INT_MAX, false, QString()});
auto [rightPos, rightVisible, rightGroup] = m_settingsData.value(rhsSymbol, {INT_MAX, false, QString()});
// check if visible
leftPos = leftVisible ? leftPos : INT_MAX;
rightPos = rightVisible ? rightPos : INT_MAX;
return leftPos <= rightPos;
}
bool ManageTokensController::filterAcceptsSymbol(const QString& symbol) const
{
const auto& [pos, visible, groupId] = m_settingsData.value(symbol, {INT_MAX, false, QString()});
return visible;
}
void ManageTokensController::classBegin()
{
// empty on purpose

View File

@ -36,10 +36,10 @@ public:
Q_INVOKABLE void saveSettings();
Q_INVOKABLE void clearSettings();
Q_INVOKABLE void revert();
Q_INVOKABLE bool hasSettings() const;
// TODO: to be used by SFPM on the main wallet page as an "expressionRole"
// bool lessThan(lhsSymbol, rhsSymbol) const;
// bool filterAcceptsRow(index or symbol?) const;
Q_INVOKABLE bool lessThan(const QString& lhsSymbol, const QString& rhsSymbol) const;
Q_INVOKABLE bool filterAcceptsSymbol(const QString& symbol) const;
protected:
void classBegin() override;
@ -85,6 +85,7 @@ private:
QString m_settingsKey;
QString settingsKey() const;
QString settingsGroupName() const;
void setSettingsKey(const QString& newSettingsKey);
QSettings m_settings;
void loadSettings();

View File

@ -23,6 +23,7 @@ const auto kEnabledNetworkCurrencyBalanceRoleName = QByteArrayLiteral("enabledNe
const auto kCustomSortOrderNoRoleName = QByteArrayLiteral("customSortOrderNo");
const auto kTokenImageRoleName = QByteArrayLiteral("imageUrl");
const auto kBackgroundColorRoleName = QByteArrayLiteral("backgroundColor");
// TODO add communityPrivilegesLevel for collectibles
} // namespace
struct TokenData {

View File

@ -123,8 +123,8 @@ StatusStackModal {
}
IssuePill {
id: issuePill
type: root.communitiesStore.discordImportErrorsCount ? IssuePill.Type.Error : IssuePill.Type.Warning
count: root.communitiesStore.discordImportErrorsCount || root.communitiesStore.discordImportWarningsCount || 0
type: root.store.discordImportErrorsCount ? IssuePill.Type.Error : IssuePill.Type.Warning
count: root.store.discordImportErrorsCount || root.store.discordImportWarningsCount || 0
visible: !!count && !fileListView.fileListModelEmpty
}
StatusButton {

View File

@ -80,7 +80,7 @@ StatusSectionLayout {
store: root.store
anchors.fill: parent
onMenuItemClicked: {
if (profileContainer.currentItem.dirty) {
if (profileContainer.currentItem.dirty && !profileContainer.currentItem.ignoreDirty) {
event.accepted = true;
profileContainer.currentItem.notifyDirty();
}

View File

@ -21,11 +21,12 @@ StatusListView {
signal itemClicked(string key)
implicitHeight: contentHeight
model: root.sourcesOfTokensModel
spacing: 8
delegate: StatusListItem {
height: 76
width: parent.width
width: ListView.view.width
title: model.name
subTitle: qsTr("%n token(s) · Last updated %1 @%2",
"",

View File

@ -9,7 +9,7 @@ import StatusQ.Core 0.1
import StatusQ.Controls 0.1
import StatusQ.Core.Theme 0.1
Item {
FocusScope {
id: root
property string sectionTitle
@ -24,10 +24,13 @@ Item {
property alias titleLayout: titleLayout
property bool dirty: false
property bool ignoreDirty // ignore dirty state and do not notifyDirty()
property bool saveChangesButtonEnabled: false
readonly property alias toast: settingsDirtyToastMessage
signal baseAreaClicked()
signal saveChangesClicked()
signal saveForLaterClicked()
signal resetChangesClicked()
function notifyDirty() {
@ -112,15 +115,17 @@ Item {
Column {
id: contentWrapper
onVisibleChanged: if (visible) forceActiveFocus()
}
}
Item {
// This is a settingsDirtyToastMessage placeholder
width: settingsDirtyToastMessage.implicitWidth
height: settingsDirtyToastMessage.active ? settingsDirtyToastMessage.implicitHeight : 0
height: settingsDirtyToastMessage.active && !root.ignoreDirty ? settingsDirtyToastMessage.implicitHeight : 0
Behavior on implicitHeight {
enabled: !root.ignoreDirty
NumberAnimation {
duration: 150
easing.type: Easing.InOutQuad
@ -133,11 +138,13 @@ Item {
SettingsDirtyToastMessage {
id: settingsDirtyToastMessage
anchors.bottom: scrollView.bottom
anchors.bottomMargin: root.ignoreDirty ? 40 : 0
anchors.horizontalCenter: scrollView.horizontalCenter
active: root.dirty
flickable: scrollView.flickable
flickable: root.ignoreDirty ? null : scrollView.flickable
saveChangesButtonEnabled: root.saveChangesButtonEnabled
onResetChangesClicked: root.resetChangesClicked()
onSaveChangesClicked: root.saveChangesClicked()
onSaveForLaterClicked: root.saveForLaterClicked()
}
}

View File

@ -29,11 +29,11 @@ SettingsContentBase {
property var walletStore
required property TokensStore tokensStore
readonly property int mainViewIndex: 0;
readonly property int networksViewIndex: 1;
readonly property int editNetworksViewIndex: 2;
readonly property int accountOrderViewIndex: 3;
readonly property int accountViewIndex: 4;
readonly property int mainViewIndex: 0
readonly property int networksViewIndex: 1
readonly property int editNetworksViewIndex: 2
readonly property int accountOrderViewIndex: 3
readonly property int accountViewIndex: 4
readonly property int manageTokensViewIndex: 5
readonly property string walletSectionTitle: qsTr("Wallet")
@ -48,6 +48,34 @@ SettingsContentBase {
}
}
dirty: manageTokensView.dirty
ignoreDirty: stackContainer.currentIndex === manageTokensViewIndex
saveChangesButtonEnabled: dirty
toast.type: SettingsDirtyToastMessage.Type.Info
toast.cancelButtonVisible: false
toast.saveForLaterButtonVisible: dirty
toast.saveChangesText: qsTr("Apply to my Wallet")
toast.changesDetectedText: qsTr("New custom sort order created")
onSaveForLaterClicked: {
manageTokensView.saveChanges()
}
onSaveChangesClicked: {
manageTokensView.saveChanges()
Global.displayToastMessage(
qsTr("Your new custom asset 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",
false,
Constants.ephemeralNotificationType.success,
""
)
}
onResetChangesClicked: {
manageTokensView.resetChanges()
}
StackLayout {
id: stackContainer
@ -55,7 +83,9 @@ SettingsContentBase {
height: stackContainer.currentIndex === root.mainViewIndex ? main.height:
stackContainer.currentIndex === root.networksViewIndex ? networksView.height:
stackContainer.currentIndex === root.editNetworksViewIndex ? editNetwork.height:
stackContainer.currentIndex === root.accountOrderViewIndex ? accountOrderView.height: accountView.height
stackContainer.currentIndex === root.accountOrderViewIndex ? accountOrderView.height:
stackContainer.currentIndex === root.manageTokensViewIndex ? manageTokensView.implicitHeight :
accountView.height
currentIndex: mainViewIndex
onCurrentIndexChanged: {
@ -218,12 +248,15 @@ SettingsContentBase {
}
ManageTokensView {
Layout.fillWidth: true
Layout.leftMargin: Style.current.padding
Layout.rightMargin: Style.current.padding
id: manageTokensView
sourcesOfTokensModel: tokensStore.sourcesOfTokensModel
tokensListModel: tokensStore.extendedFlatTokensModel
baseWalletAssetsModel: RootStore.assets // TODO include community assets (#12369)
baseWalletCollectiblesModel: {
RootStore.setFillterAllAddresses() // FIXME no other way to get _all_ collectibles?
// TODO concat proxy model to include community collectibles (#12519)
return RootStore.collectiblesStore.ownedCollectibles
}
}
DappPermissionsView {

View File

@ -1,5 +1,4 @@
import QtQuick 2.15
import SortFilterProxyModel 0.2
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
@ -38,7 +37,6 @@ ColumnLayout {
text: accountsList.count > 1? qsTr("Move your most frequently used accounts to the top of your wallet list") :
qsTr("This account looks a little lonely. Add another account to enable re-ordering.")
color: Theme.palette.baseColor1
font.pixelSize: Style.current.primaryTextFontSize
}
StatusListView {
@ -102,7 +100,6 @@ ColumnLayout {
icon.height: 40
icon.name: model.emoji
icon.color: Utils.getColorForId(model.colorId)
actions: []
}
}
}

View File

@ -1,14 +1,13 @@
import QtQuick 2.13
import SortFilterProxyModel 0.2
import utils 1.0
import shared.status 1.0
import shared.panels 1.0
import StatusQ.Core.Theme 0.1
import StatusQ.Core 0.1
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import utils 1.0
import shared.status 1.0
import shared.panels 1.0
import shared.popups 1.0
import shared.popups.addaccount 1.0

View File

@ -4,8 +4,10 @@ import QtQuick.Layouts 1.15
import StatusQ.Controls 0.1
import shared.controls 1.0
import utils 1.0
import AppLayouts.Profile.panels 1.0
import AppLayouts.Wallet.panels 1.0
ColumnLayout {
id: root
@ -13,6 +15,58 @@ ColumnLayout {
required property var sourcesOfTokensModel // Expected roles: key, name, updatedAt, source, version, tokensCount, image
required property var tokensListModel // Expected roles: name, symbol, image, chainName, explorerUrl
required property var baseWalletAssetsModel
required property var baseWalletCollectiblesModel
readonly property bool dirty: {
if (!loader.item)
return false
if (tabBar.currentIndex > d.collectiblesTabIndex)
return false
if (tabBar.currentIndex === d.collectiblesTabIndex && baseCollectiblesModel.isFetching)
return false
return loader.item && loader.item.dirty
}
function saveChanges() {
if (tabBar.currentIndex > d.collectiblesTabIndex)
return
loader.item.saveSettings()
}
function resetChanges() {
if (tabBar.currentIndex > d.collectiblesTabIndex)
return
loader.item.revert()
}
QtObject {
id: d
readonly property int assetsTabIndex: 0
readonly property int collectiblesTabIndex: 1
readonly property int tokenSourcesTabIndex: 2
function checkLoadMoreCollectibles() {
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)
return
root.baseCollectiblesModel.loadMore()
}
}
Connections {
target: root.baseCollectiblesModel
function onHasMoreChanged() {
d.checkLoadMoreCollectibles()
}
function onIsFetchingChanged() {
d.checkLoadMoreCollectibles()
}
}
StatusTabBar {
id: tabBar
@ -20,49 +74,60 @@ ColumnLayout {
Layout.topMargin: 5
StatusTabButton {
id: assetsTab
leftPadding: 0
width: implicitWidth
text: qsTr("Assets")
}
StatusTabButton {
id: collectiblesTab
width: implicitWidth
text: qsTr("Collectibles")
}
StatusTabButton {
id: tokensListTab
width: implicitWidth
text: qsTr("Token lists")
}
}
StackLayout {
id: stackLayout
// NB: we want to discard any pending unsaved changes when switching tabs or navigating away
Loader {
id: loader
Layout.fillWidth: true
Layout.fillHeight: true
currentIndex: tabBar.currentIndex
active: visible
ShapeRectangle {
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: parent.width - 4 // The rectangular path is rendered outside
Layout.maximumHeight: 44
text: qsTr("Youll be able to manage the display of your assets here")
sourceComponent: {
switch (tabBar.currentIndex) {
case d.assetsTabIndex:
return tokensPanel
case d.collectiblesTabIndex:
return collectiblesPanel
case d.tokenSourcesTabIndex:
return supportedTokensListPanel
}
}
}
ShapeRectangle {
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: parent.width - 4 // The rectangular path is rendered outside
Layout.maximumHeight: 44
text: qsTr("Youll be able to manage the display of your collectibles here")
Component {
id: tokensPanel
ManageAssetsPanel {
baseModel: root.baseWalletAssetsModel
}
// TODO #12611 add Advanced section
}
Component {
id: collectiblesPanel
ManageCollectiblesPanel {
baseModel: root.baseWalletCollectiblesModel
Component.onCompleted: d.checkLoadMoreCollectibles()
}
}
Component {
id: supportedTokensListPanel
SupportedTokenListsPanel {
Layout.fillWidth: true
Layout.fillHeight: true
sourcesOfTokensModel: root.sourcesOfTokensModel
tokensListModel: root.tokensListModel
}

View File

@ -13,6 +13,7 @@ DropArea {
objectName: "manageTokensDelegate-%1".arg(index)
// expected roles: symbol, name, communityId, communityName, communityImage, collectionName, imageUrl
// + enabledNetworkBalance, enabledNetworkCurrencyBalance -> TODO might get dropped/renamed in the future!!!
property var controller
property int visualIndex: index
@ -71,7 +72,7 @@ DropArea {
: LocaleUtils.currencyAmountToLocaleString(model.enabledNetworkBalance)
bgRadius: priv.bgRadius
hasImage: true
icon.source: root.isCollectible ? model.imageUrl : Constants.tokenIcon(model.symbol) // TODO unify via backend model for both assets and collectibles
icon.source: root.isCollectible ? model.imageUrl : Constants.tokenIcon(model.symbol) // TODO unify via backend model for both assets and collectibles; handle communityPrivilegesLevel
icon.width: priv.iconSize
icon.height: priv.iconSize
spacing: 12

View File

@ -18,8 +18,8 @@ ColumnLayout {
property int outNetworkLayer: 0
property int inNetworkLayer: 0
property int outNetworkTimestamp: 0
property int inNetworkTimestamp: 0
property double outNetworkTimestamp: 0
property double inNetworkTimestamp: 0
property string outChainName
property string inChainName

View File

@ -2,5 +2,5 @@ WalletHeader 1.0 WalletHeader.qml
WalletTxProgressBlock 1.0 WalletTxProgressBlock.qml
WalletNftPreview 1.0 WalletNftPreview.qml
ActivityFilterPanel 1.0 ActivityFilterPanel.qml
ManageTokensPanel 1.0 ManageTokensPanel.qml
ManageAssetsPanel 1.0 ManageAssetsPanel.qml
ManageCollectiblesPanel 1.0 ManageCollectiblesPanel.qml

View File

@ -38,7 +38,8 @@ QtObject {
property CollectiblesStore collectiblesStore: CollectiblesStore {}
property var areTestNetworksEnabled: networksModule.areTestNetworksEnabled
readonly property bool areTestNetworksEnabled: networksModule.areTestNetworksEnabled
readonly property bool isSepoliaEnabled: networksModule.isSepoliaEnabled
property var savedAddresses: SortFilterProxyModel {
sourceModel: walletSectionSavedAddresses.model

View File

@ -40,7 +40,6 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter
color: Style.current.secondaryText
text: qsTr("Collectibles will appear here")
font.pixelSize: 15
}
}
}
@ -65,7 +64,7 @@ Item {
isLoading: !!model.isLoading
privilegesLevel: model.communityPrivilegesLevel ?? Constants.TokenPrivilegesLevel.Community
ornamentColor: model.communityColor ?? "transparent"
communityId: model.communityId
communityId: model.communityId ?? ""
onClicked: root.collectibleClicked(model.chainId, model.contractAddress, model.tokenId, model.uid)
}

View File

@ -156,8 +156,6 @@ Item {
}
}
CollectibleDetailView {
Layout.fillWidth: true
Layout.fillHeight: true
collectible: RootStore.collectiblesStore.detailedCollectible
isCollectibleLoading: RootStore.collectiblesStore.isDetailedCollectibleLoading
@ -169,8 +167,6 @@ Item {
AssetsDetailView {
id: assetDetailView
Layout.fillWidth: true
Layout.fillHeight: true
visible: (stack.currentIndex === 2)
assetsLoading: RootStore.assetsLoading
@ -186,8 +182,6 @@ Item {
TransactionDetailView {
id: transactionDetailView
Layout.fillWidth: true
Layout.fillHeight: true
onVisibleChanged: {
if (!visible)
transaction = null

View File

@ -1612,6 +1612,7 @@ Item {
this.open = false
}
onLinkActivated: {
this.open = false
if(actionRequired) {
toastsManager.doAction(model.actionType, model.actionData)
return

View File

@ -1,6 +1,6 @@
import QtQuick 2.14
import QtQuick.Layouts 1.14
import QtGraphicalEffects 1.13
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtGraphicalEffects 1.15
import utils 1.0
@ -12,14 +12,24 @@ Rectangle {
id: root
property bool active: false
property bool cancelButtonVisible: true
property bool saveChangesButtonEnabled: false
property bool saveForLaterButtonVisible
property alias saveChangesText: saveChangesButton.text
property alias saveForLaterText: saveForLaterButton.text
property alias cancelChangesText: cancelChangesButton.text
property alias changesDetectedText: changesDetectedTextItem.text
property Flickable flickable: null
enum Type {
Danger,
Info
}
property int type: SettingsDirtyToastMessage.Type.Danger
signal saveChangesClicked
signal saveForLaterClicked
signal resetChangesClicked
function notifyDirty() {
@ -33,8 +43,9 @@ Rectangle {
opacity: active ? 1 : 0
color: Theme.palette.statusToastMessage.backgroundColor
radius: 8
border.color: Theme.palette.dangerColor2
border.color: type === SettingsDirtyToastMessage.Type.Danger ? Theme.palette.dangerColor2 : Theme.palette.primaryColor2
border.width: 2
layer.enabled: true
layer.effect: DropShadow {
verticalOffset: 3
@ -42,7 +53,7 @@ Rectangle {
samples: 15
fast: true
cached: true
color: Theme.palette.dangerColor2
color: root.border.color
spread: 0.1
}
@ -104,7 +115,6 @@ Rectangle {
StatusBaseText {
id: changesDetectedTextItem
Layout.columnSpan: 2
Layout.fillWidth: true
padding: 8
horizontalAlignment: Text.AlignHCenter
@ -116,10 +126,19 @@ Rectangle {
id: cancelChangesButton
text: qsTr("Cancel")
enabled: root.active
visible: root.cancelButtonVisible
type: StatusBaseButton.Type.Danger
onClicked: root.resetChangesClicked()
}
StatusFlatButton {
id: saveForLaterButton
text: qsTr("Save for later")
enabled: root.active && root.saveChangesButtonEnabled
visible: root.saveForLaterButtonVisible
onClicked: root.saveForLaterClicked()
}
StatusButton {
id: saveChangesButton
objectName: "settingsDirtyToastMessageSaveButton"