diff --git a/storybook/pages/BrowserLayoutPage.qml b/storybook/pages/BrowserLayoutPage.qml index d943a4143c7..c67a7cb440b 100644 --- a/storybook/pages/BrowserLayoutPage.qml +++ b/storybook/pages/BrowserLayoutPage.qml @@ -10,7 +10,7 @@ import StatusQ.Core.Utils as SQUtils import utils import AppLayouts.Browser -import AppLayouts.Browser.stores as BrowserStores +import AppLayouts.stores.Browser as BrowserStores import AppLayouts.Wallet.stores import shared.stores as SharedStores import shared.stores.send diff --git a/storybook/pages/TransactionDelegatePage.qml b/storybook/pages/TransactionDelegatePage.qml index 4a7ee0992c4..292b2b39551 100644 --- a/storybook/pages/TransactionDelegatePage.qml +++ b/storybook/pages/TransactionDelegatePage.qml @@ -2,11 +2,13 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import StatusQ import StatusQ.Core import StatusQ.Core.Theme import utils import shared.controls +import shared.views import shared.stores as SharedStores import AppLayouts.Wallet.stores as WalletStores @@ -57,6 +59,35 @@ SplitView { property bool _highlight: false } + ListModel { + id: mockModel + ListElement {} + Component.onCompleted: { + mockModel.setProperty(0, "activityEntry", root.mockupModelData) + } + } + + Connections { + target: root.mockupModelData + function onStatusChanged() { mockModel.setProperty(0, "activityEntry", root.mockupModelData) } + function onTxTypeChanged() { mockModel.setProperty(0, "activityEntry", root.mockupModelData) } + function onIsMultiTransactionChanged() { mockModel.setProperty(0, "activityEntry", root.mockupModelData) } + function onIsNFTChanged() { mockModel.setProperty(0, "activityEntry", root.mockupModelData) } + } + + TransactionsModelAdaptor { + id: txAdaptor + sourceModel: mockModel + flatNetworks: NetworksModel.flatNetworks + currentCurrency: "EUR" + getFiatValueFn: (cryptoValue, symbol) => cryptoValue * 0.1 + formatCurrencyAmountFn: (cryptoValue, symbol) => "%L1 %2".arg(cryptoValue).arg(symbol) + getNameForAddressFn: (address) => WalletStores.RootStore.getNameForAddress(address) + getDappDetailsFn: (chainId, address) => WalletStores.RootStore.getDappDetails(chainId, address) + getTransactionTypeFn: (transaction) => WalletStores.RootStore.getTransactionType(transaction) + localeUtils: LocaleUtils + } + SplitView { orientation: Qt.Vertical SplitView.fillWidth: true @@ -77,24 +108,25 @@ SplitView { width: 600 - TransactionDelegate { - id: delegate + ListView { + id: listView Layout.fillWidth: true - modelData: root.mockupModelData - showAllAccounts: ctrlAllAccounts.checked - currenciesStore: SharedStores.CurrenciesStore { - readonly property string currentCurrency: "EUR" - - function getFiatValue(cryptoValue, symbol) { - return cryptoValue * 0.1; - } - - function formatCurrencyAmount(cryptoValue, symbol) { - return "%L1 %2".arg(cryptoValue).arg(symbol) - } + Layout.preferredHeight: 80 + model: txAdaptor.model + interactive: false + delegate: TransactionDelegate { + width: listView.width + showAllAccounts: ctrlAllAccounts.checked + currentCurrency: "EUR" + formatCurrencyAmountFn: (cryptoValue, symbol) => "%L1 %2".arg(cryptoValue).arg(symbol) + loading: ctrlLoading.checked + state: ctrlHeaderState.checked ? "header" : "" } - flatNetworks: NetworksModel.flatNetworks - activityStore: WalletStores.RootStore + } + + TransactionDelegate { + Layout.fillWidth: true + loading: true } } } @@ -113,15 +145,12 @@ SplitView { ColumnLayout { CheckBox { + id: ctrlLoading text: "Is loading" - checked: delegate.loading - onToggled: delegate.loading = checked } CheckBox { + id: ctrlHeaderState text: "Is activity details header" - readonly property string headerState: "header" - checked: delegate.state === headerState - onToggled: delegate.state = checked ? headerState : "" } CheckBox { diff --git a/storybook/stubs/AppLayouts/Browser/stores/BookmarksStore.qml b/storybook/stubs/AppLayouts/stores/Browser/BookmarksStore.qml similarity index 100% rename from storybook/stubs/AppLayouts/Browser/stores/BookmarksStore.qml rename to storybook/stubs/AppLayouts/stores/Browser/BookmarksStore.qml diff --git a/storybook/stubs/AppLayouts/Browser/stores/BrowserActivityStore.qml b/storybook/stubs/AppLayouts/stores/Browser/BrowserActivityStore.qml similarity index 100% rename from storybook/stubs/AppLayouts/Browser/stores/BrowserActivityStore.qml rename to storybook/stubs/AppLayouts/stores/Browser/BrowserActivityStore.qml diff --git a/storybook/stubs/AppLayouts/Browser/stores/BrowserRootStore.qml b/storybook/stubs/AppLayouts/stores/Browser/BrowserRootStore.qml similarity index 100% rename from storybook/stubs/AppLayouts/Browser/stores/BrowserRootStore.qml rename to storybook/stubs/AppLayouts/stores/Browser/BrowserRootStore.qml diff --git a/storybook/stubs/AppLayouts/Browser/stores/BrowserWalletStore.qml b/storybook/stubs/AppLayouts/stores/Browser/BrowserWalletStore.qml similarity index 100% rename from storybook/stubs/AppLayouts/Browser/stores/BrowserWalletStore.qml rename to storybook/stubs/AppLayouts/stores/Browser/BrowserWalletStore.qml diff --git a/storybook/stubs/AppLayouts/Browser/stores/DownloadsStore.qml b/storybook/stubs/AppLayouts/stores/Browser/DownloadsStore.qml similarity index 100% rename from storybook/stubs/AppLayouts/Browser/stores/DownloadsStore.qml rename to storybook/stubs/AppLayouts/stores/Browser/DownloadsStore.qml diff --git a/storybook/stubs/AppLayouts/Browser/stores/qmldir b/storybook/stubs/AppLayouts/stores/Browser/qmldir similarity index 100% rename from storybook/stubs/AppLayouts/Browser/stores/qmldir rename to storybook/stubs/AppLayouts/stores/Browser/qmldir diff --git a/ui/app/AppLayouts/Browser/+noWebEngine/BrowserLayout.qml b/ui/app/AppLayouts/Browser/+noWebEngine/BrowserLayout.qml index eef3d0e7734..126b716cd27 100644 --- a/ui/app/AppLayouts/Browser/+noWebEngine/BrowserLayout.qml +++ b/ui/app/AppLayouts/Browser/+noWebEngine/BrowserLayout.qml @@ -2,7 +2,7 @@ import QtQuick import StatusQ.Layout -import AppLayouts.Browser.stores as BrowserStores +import AppLayouts.stores.Browser as BrowserStores // dummy container/section for mobile StatusSectionLayout { diff --git a/ui/app/AppLayouts/Browser/BrowserLayout.qml b/ui/app/AppLayouts/Browser/BrowserLayout.qml index e0f1ebf36ca..2d7ac3384b7 100644 --- a/ui/app/AppLayouts/Browser/BrowserLayout.qml +++ b/ui/app/AppLayouts/Browser/BrowserLayout.qml @@ -20,7 +20,8 @@ import shared.popups.send import shared.stores.send import shared.stores as SharedStores -import AppLayouts.Browser.stores as BrowserStores +import AppLayouts.stores.Browser as BrowserStores +import AppLayouts.Browser.adaptors import AppLayouts.Wallet.services.dapps import "provider/qml" @@ -113,6 +114,11 @@ StatusSectionLayout { property Item currentWebView: tabs.currentIndex < tabs.count ? tabs.getCurrentTab() : null + readonly property var walletMenuAdaptor: BrowserWalletMenuAdaptor { + activeNetworksModel: root.networksStore.activeNetworks + currentAccount: root.browserWalletStore.dappBrowserAccount + } + property Component browserDialogComponent: BrowserDialog {} property Component jsDialogComponent: JSDialogWindow {} @@ -269,16 +275,13 @@ StatusSectionLayout { _internal.currentWebView.url = _internal.determineRealURL(url); } onOpenWalletMenu: { - // Initialize activity filters before opening popup - const activeChainIds = SQUtils.ModelUtils.modelToFlatArray( - root.networksStore.activeNetworks, "chainId") - if (activeChainIds.length > 0) { + // Initialize filters before opening popup + if (_internal.walletMenuAdaptor.hasActiveChains) { root.browserActivityStore.activityController.setFilterChainsJson( - JSON.stringify(activeChainIds), true) + _internal.walletMenuAdaptor.chainsFilterJson, true) } - const currentAddress = root.browserWalletStore.dappBrowserAccount.address root.browserActivityStore.activityController.setFilterAddressesJson( - JSON.stringify([currentAddress])) + _internal.walletMenuAdaptor.addressesFilterJson) Global.openPopup(browserWalletMenu) } @@ -445,23 +448,42 @@ StatusSectionLayout { incognitoMode: _internal.currentWebView && _internal.currentWebView.profile === connectorBridge.otrProfile accounts: root.browserWalletStore.accounts currentAccount: root.browserWalletStore.dappBrowserAccount - activityStore: root.browserActivityStore - currencyStore: root.currencyStore - networksStore: root.networksStore -// property point headerPoint: Qt.point(browserHeader.x, browserHeader.y) -// x: (parent.width - width - Theme.halfPadding) -// y: (Math.abs(browserHeader.mapFromGlobal(headerPoint).y) + -// browserHeader.anchors.topMargin + Theme.halfPadding) + loadingHistoryTransactions: root.browserActivityStore.loadingHistoryTransactions + historyTransactionsModel: root.browserActivityStore.historyTransactions + newDataAvailable: root.browserActivityStore.newDataAvailable + isNonArchivalNode: root.browserActivityStore.isNonArchivalNode + selectedAddress: root.browserActivityStore.selectedAddress + isFilterDirty: root.browserActivityStore.transactionActivityStatus.isFilterDirty + + activeNetworks: root.networksStore.activeNetworks + allNetworks: root.networksStore.allNetworks + + currentCurrency: root.currencyStore.currentCurrency + + getNameForAddressFn: function(address) { + return root.browserActivityStore.getNameForAddress(address) + } + getDappDetailsFn: function(chainId, address) { + return root.browserActivityStore.getDappDetails(chainId, address) + } + getFiatValueFn: function(amount, symbol) { + return root.currencyStore.getFiatValue(amount, symbol) + } + formatCurrencyAmountFn: function(amount, symbol, options) { + return root.currencyStore.formatCurrencyAmount(amount, symbol, options) + } + getTransactionTypeFn: function(transaction) { + return root.browserActivityStore.getTransactionType(transaction) + } onSendTriggered: (address) => root.sendToRecipientRequested(address) onAccountChanged: (newAddress) => connectorBridge.connectorManager.changeAccount(newAddress) onReload: { - for (let i = 0; i < tabs.count; ++i){ - tabs.getTab(i).reload(); + for (let i = 0; i < tabs.count; ++i) { + tabs.getTab(i).reload() } } - onAccountSwitchRequested: (address) => { root.browserWalletStore.switchAccountByAddress(address) } @@ -469,6 +491,13 @@ StatusSectionLayout { root.browserActivityStore.activityController.setFilterAddressesJson(addressesJson) } + onUpdateTransactionFilterRequested: root.browserActivityStore.updateTransactionFilterIfDirty() + onFetchMoreTransactionsRequested: root.browserActivityStore.fetchMoreTransactions() + onResetActivityDataRequested: root.browserActivityStore.resetActivityData() + onUpdateCollectiblesModelRequested: root.browserActivityStore.currentActivityFiltersStore.updateCollectiblesModel() + onUpdateRecipientsModelRequested: root.browserActivityStore.currentActivityFiltersStore.updateRecipientsModel() + onApplyAllFiltersRequested: root.browserActivityStore.currentActivityFiltersStore.applyAllFilters() + Connections { target: root.browserActivityStore.transactionActivityStatus enabled: visible diff --git a/ui/app/AppLayouts/Browser/adaptors/BrowserWalletMenuAdaptor.qml b/ui/app/AppLayouts/Browser/adaptors/BrowserWalletMenuAdaptor.qml new file mode 100644 index 00000000000..2031b0238ed --- /dev/null +++ b/ui/app/AppLayouts/Browser/adaptors/BrowserWalletMenuAdaptor.qml @@ -0,0 +1,50 @@ +import QtQuick +import StatusQ.Core.Utils as SQUtils +import utils + +/** + * BrowserWalletMenuAdaptor + * + * Prepares data for initializing the browser wallet menu popup. + * Transforms store data into filter-ready formats. + * + * Input: + * - activeNetworksModel: Model of active networks from NetworksStore + * - currentAccount: Current browser account object + * + * Output: + * - activeChainIds: Array of active chain IDs + * - chainsFilterJson: JSON string of chain IDs for filtering + * - addressesFilterJson: JSON string of current address for filtering + * - hasActiveChains: Whether there are any active chains + */ +QtObject { + id: root + + required property var activeNetworksModel + required property var currentAccount + + readonly property var activeChainIds: { + if (!root.activeNetworksModel) { + return [] + } + return SQUtils.ModelUtils.modelToFlatArray( + root.activeNetworksModel, "chainId") + } + + readonly property string currentAccountAddress: { + return root.currentAccount?.address ?? "" + } + + readonly property string chainsFilterJson: { + return JSON.stringify(root.activeChainIds) + } + + readonly property string addressesFilterJson: { + return JSON.stringify([root.currentAccountAddress]) + } + + readonly property bool hasActiveChains: { + return root.activeChainIds.length > 0 + } +} diff --git a/ui/app/AppLayouts/Browser/adaptors/qmldir b/ui/app/AppLayouts/Browser/adaptors/qmldir new file mode 100644 index 00000000000..f132690e670 --- /dev/null +++ b/ui/app/AppLayouts/Browser/adaptors/qmldir @@ -0,0 +1 @@ +BrowserWalletMenuAdaptor 1.0 BrowserWalletMenuAdaptor.qml diff --git a/ui/app/AppLayouts/Browser/popups/AddFavoriteModal.qml b/ui/app/AppLayouts/Browser/popups/AddFavoriteModal.qml index f320c7552b5..a0c9947e6ac 100644 --- a/ui/app/AppLayouts/Browser/popups/AddFavoriteModal.qml +++ b/ui/app/AppLayouts/Browser/popups/AddFavoriteModal.qml @@ -11,7 +11,7 @@ import shared.popups import utils -import AppLayouts.Browser.stores as BrowserStores +import AppLayouts.stores.Browser as BrowserStores // TODO: replace with StatusDialog ModalPopup { diff --git a/ui/app/AppLayouts/Browser/popups/BrowserConnectionModal.qml b/ui/app/AppLayouts/Browser/popups/BrowserConnectionModal.qml index 20e488917b2..1e7c1338fb5 100644 --- a/ui/app/AppLayouts/Browser/popups/BrowserConnectionModal.qml +++ b/ui/app/AppLayouts/Browser/popups/BrowserConnectionModal.qml @@ -11,7 +11,7 @@ import utils import shared.panels import shared.controls -import AppLayouts.Browser.stores as BrowserStores +import AppLayouts.stores.Browser as BrowserStores import "../controls" diff --git a/ui/app/AppLayouts/Browser/popups/BrowserWalletMenu.qml b/ui/app/AppLayouts/Browser/popups/BrowserWalletMenu.qml index fdd8f0bb248..a88abeda831 100644 --- a/ui/app/AppLayouts/Browser/popups/BrowserWalletMenu.qml +++ b/ui/app/AppLayouts/Browser/popups/BrowserWalletMenu.qml @@ -13,24 +13,45 @@ import shared.views import shared.stores as SharedStores import utils +import AppLayouts.Browser.adaptors + // TODO: replace with StatusMenu Dialog { id: root required property bool incognitoMode - // model of wallet accounts for the dropdown required property var accounts - // currently selected wallet account required property var currentAccount - required property var activityStore - required property SharedStores.CurrenciesStore currencyStore - required property SharedStores.NetworksStore networksStore + + required property bool loadingHistoryTransactions + required property var historyTransactionsModel + required property bool newDataAvailable + required property bool isNonArchivalNode + required property string selectedAddress + required property bool isFilterDirty + + required property var activeNetworks + required property var allNetworks + + required property string currentCurrency + + required property var getNameForAddressFn + required property var getDappDetailsFn + required property var getFiatValueFn + required property var formatCurrencyAmountFn + required property var getTransactionTypeFn signal sendTriggered(string address) signal reload() signal accountChanged(string newAddress) signal accountSwitchRequested(string address) signal filterAddressesChangeRequested(string addressesJson) + signal updateTransactionFilterRequested() + signal fetchMoreTransactionsRequested() + signal resetActivityDataRequested() + signal updateCollectiblesModelRequested() + signal updateRecipientsModelRequested() + signal applyAllFiltersRequested() modal: false @@ -193,16 +214,37 @@ Dialog { anchors.topMargin: Theme.bigPadding anchors.bottom: parent.bottom - activityStore: root.activityStore overview: root.currentAccount - communitiesStore: null - currencyStore: root.currencyStore - networksStore: root.networksStore + loadingHistoryTransactions: root.loadingHistoryTransactions + historyTransactionsModel: root.historyTransactionsModel + newDataAvailable: root.newDataAvailable + isNonArchivalNode: root.isNonArchivalNode + selectedAddress: root.selectedAddress showAllAccounts: false + isFilterDirty: root.isFilterDirty + activeNetworks: root.activeNetworks + allNetworks: root.allNetworks + currentCurrency: root.currentCurrency + + getNameForAddressFn: root.getNameForAddressFn + getDappDetailsFn: root.getDappDetailsFn + getFiatValueFn: root.getFiatValueFn + formatCurrencyAmountFn: root.formatCurrencyAmountFn + getTransactionTypeFn: root.getTransactionTypeFn + + communitiesStore: null + displayValues: true filterVisible: false disableShadowOnScroll: true hideVerticalScrollbar: false + + onUpdateTransactionFilterRequested: root.updateTransactionFilterRequested() + onMoreTransactionsRequested: root.fetchMoreTransactionsRequested() + onActivityDataResetRequested: root.resetActivityDataRequested() + onCollectiblesModelUpdateRequested: root.updateCollectiblesModelRequested() + onRecipientsModelUpdateRequested: root.updateRecipientsModelRequested() + onAllFiltersApplyRequested: root.applyAllFiltersRequested() } onClosed: { root.destroy(); diff --git a/ui/app/AppLayouts/Browser/popups/DownloadMenu.qml b/ui/app/AppLayouts/Browser/popups/DownloadMenu.qml index aa8e2179691..1de7918440b 100644 --- a/ui/app/AppLayouts/Browser/popups/DownloadMenu.qml +++ b/ui/app/AppLayouts/Browser/popups/DownloadMenu.qml @@ -4,7 +4,7 @@ import QtWebEngine import StatusQ.Popups -import AppLayouts.Browser.stores as BrowserStores +import AppLayouts.stores.Browser as BrowserStores StatusMenu { id: root diff --git a/ui/app/AppLayouts/Browser/popups/FavoriteMenu.qml b/ui/app/AppLayouts/Browser/popups/FavoriteMenu.qml index 5d1ee58ab5b..33259def2d0 100644 --- a/ui/app/AppLayouts/Browser/popups/FavoriteMenu.qml +++ b/ui/app/AppLayouts/Browser/popups/FavoriteMenu.qml @@ -4,7 +4,7 @@ import QtQuick.Controls import StatusQ.Popups import StatusQ.Core.Theme -import AppLayouts.Browser.stores as BrowserStores +import AppLayouts.stores.Browser as BrowserStores StatusMenu { id: root diff --git a/ui/app/AppLayouts/Browser/popups/SignMessageModal.qml b/ui/app/AppLayouts/Browser/popups/SignMessageModal.qml index 832152b3877..f58feba3e32 100644 --- a/ui/app/AppLayouts/Browser/popups/SignMessageModal.qml +++ b/ui/app/AppLayouts/Browser/popups/SignMessageModal.qml @@ -13,7 +13,7 @@ import shared.views import shared.panels import shared.popups -import AppLayouts.Browser.stores as BrowserStores +import AppLayouts.stores.Browser as BrowserStores StatusModal { id: root diff --git a/ui/app/AppLayouts/Browser/views/BrowserWebEngineView.qml b/ui/app/AppLayouts/Browser/views/BrowserWebEngineView.qml index cf0d20e54c0..a0159f7004f 100644 --- a/ui/app/AppLayouts/Browser/views/BrowserWebEngineView.qml +++ b/ui/app/AppLayouts/Browser/views/BrowserWebEngineView.qml @@ -10,7 +10,7 @@ import utils import "../panels" import "ScriptUtils.js" as ScriptUtils -import AppLayouts.Browser.stores as BrowserStores +import AppLayouts.stores.Browser as BrowserStores WebEngineView { id: root diff --git a/ui/app/AppLayouts/Wallet/popups/SavedAddressActivityPopup.qml b/ui/app/AppLayouts/Wallet/popups/SavedAddressActivityPopup.qml index b1cee5cdbbc..ecc9c217b6c 100644 --- a/ui/app/AppLayouts/Wallet/popups/SavedAddressActivityPopup.qml +++ b/ui/app/AppLayouts/Wallet/popups/SavedAddressActivityPopup.qml @@ -229,8 +229,39 @@ StatusDialog { isWatchOnlyAccount: false, mixedcaseAddress: d.address }) - activityStore: WalletStore.RootStore - networksStore: root.networksStore + + loadingHistoryTransactions: WalletStore.RootStore.loadingHistoryTransactions + historyTransactionsModel: WalletStore.RootStore.historyTransactions + newDataAvailable: WalletStore.RootStore.newDataAvailable + isNonArchivalNode: WalletStore.RootStore.isNonArchivalNode + selectedAddress: d.address + isFilterDirty: WalletStore.RootStore.activityController.activityFilterStore.isFilterDirty + activeNetworks: root.networksStore.activeNetworks + allNetworks: root.networksStore.allNetworks + currentCurrency: WalletStore.RootStore.getCurrencyAmount.symbol || "" + + getNameForAddressFn: function(address) { + return WalletStore.RootStore.getNameForAddress(address) + } + getDappDetailsFn: function(chainId, address) { + return WalletStore.RootStore.getDappDetails(chainId, address) + } + getFiatValueFn: function(amount, symbol) { + return root.sharedRootStore.currencyStore.getFiatValue(amount, symbol) + } + formatCurrencyAmountFn: function(amount, symbol, options) { + return root.sharedRootStore.currencyStore.formatCurrencyAmount(amount, symbol, options) + } + getTransactionTypeFn: function(transaction) { + return WalletStore.RootStore.getTransactionType(transaction) + } + + onUpdateTransactionFilterRequested: WalletStore.RootStore.updateTransactionFilterIfDirty() + onMoreTransactionsRequested: WalletStore.RootStore.fetchMoreTransactions() + onActivityDataResetRequested: WalletStore.RootStore.resetActivityData() + onCollectiblesModelUpdateRequested: WalletStore.RootStore.activityController.activityFilterStore.updateCollectiblesModel() + onRecipientsModelUpdateRequested: WalletStore.RootStore.activityController.activityFilterStore.updateRecipientsModel() + onAllFiltersApplyRequested: WalletStore.RootStore.activityController.activityFilterStore.applyAllFilters() } } } diff --git a/ui/app/AppLayouts/Wallet/views/RightTabView.qml b/ui/app/AppLayouts/Wallet/views/RightTabView.qml index fa2a5e81aa0..91923068334 100644 --- a/ui/app/AppLayouts/Wallet/views/RightTabView.qml +++ b/ui/app/AppLayouts/Wallet/views/RightTabView.qml @@ -532,13 +532,57 @@ RightTabBaseView { id: historyView HistoryView { overview: RootStore.overview - activityStore: RootStore - communitiesStore: root.communitiesStore - currencyStore: root.sharedRootStore.currencyStore - networksStore: root.networksStore + + loadingHistoryTransactions: RootStore.loadingHistoryTransactions + historyTransactionsModel: RootStore.historyTransactions + newDataAvailable: RootStore.newDataAvailable + isNonArchivalNode: RootStore.isNonArchivalNode + selectedAddress: RootStore.overview.address showAllAccounts: RootStore.showAllAccounts + isFilterDirty: RootStore.activityController.activityFilterStore.isFilterDirty + activeNetworks: root.networksStore.activeNetworks + allNetworks: root.networksStore.allNetworks + currentCurrency: root.sharedRootStore.currencyStore.currentCurrency + + getNameForAddressFn: function(address) { + return RootStore.getNameForAddress(address) + } + getDappDetailsFn: function(chainId, address) { + return RootStore.getDappDetails(chainId, address) + } + getFiatValueFn: function(amount, symbol) { + return root.sharedRootStore.currencyStore.getFiatValue(amount, symbol) + } + formatCurrencyAmountFn: function(amount, symbol, options) { + return root.sharedRootStore.currencyStore.formatCurrencyAmount(amount, symbol, options) + } + getTransactionTypeFn: function(transaction) { + return RootStore.getTransactionType(transaction) + } + + communitiesStore: root.communitiesStore + filterVisible: false // TODO #16761: Re-enable filter for activity when implemented bannerComponent: buyReceiveBannerComponent + + onUpdateTransactionFilterRequested: RootStore.updateTransactionFilterIfDirty() + onMoreTransactionsRequested: RootStore.fetchMoreTransactions() + onActivityDataResetRequested: RootStore.resetActivityData() + onCollectiblesModelUpdateRequested: RootStore.activityController.activityFilterStore.updateCollectiblesModel() + onRecipientsModelUpdateRequested: RootStore.activityController.activityFilterStore.updateRecipientsModel() + onAllFiltersApplyRequested: RootStore.activityController.activityFilterStore.applyAllFilters() + + Connections { + target: RootStore.transactionActivityStatus + enabled: visible + function onIsFilterDirtyChanged() { + RootStore.updateTransactionFilterIfDirty() + } + function onFilterChainsChanged() { + RootStore.activityController.activityFilterStore.updateCollectiblesModel() + RootStore.activityController.activityFilterStore.updateRecipientsModel() + } + } } } } diff --git a/ui/app/AppLayouts/Wallet/views/collectibles/CollectibleDetailView.qml b/ui/app/AppLayouts/Wallet/views/collectibles/CollectibleDetailView.qml index 0284c80282e..c4e08a40c4f 100644 --- a/ui/app/AppLayouts/Wallet/views/collectibles/CollectibleDetailView.qml +++ b/ui/app/AppLayouts/Wallet/views/collectibles/CollectibleDetailView.qml @@ -277,32 +277,44 @@ Item { Component { id: activityView - StatusListView { + Item { height: scrollView.availableHeight - model: root.activityModel - header: ShapeRectangle { - width: parent.width - height: visible ? 42: 0 - visible: !root.activityModel.count && !d.activityLoading - font.pixelSize: Theme.primaryTextFontSize - text: qsTr("Activity will appear here") - } - delegate: TransactionDelegate { - required property var model - required property int index - width: ListView.view.width - modelData: model.activityEntry - timeStampText: isModelDataValid ? LocaleUtils.formatRelativeTimestamp(modelData.timestamp * 1000, true) : "" + + TransactionsModelAdaptor { + id: activityModelAdaptor + sourceModel: root.activityModel flatNetworks: root.networksStore.allNetworks - currenciesStore: root.rootStore.currencyStore - activityStore: root.walletRootStore - showAllAccounts: root.walletRootStore.showAllAccounts - displayValues: true - community: isModelDataValid && !!communityId && !!root.communitiesStore ? root.communitiesStore.getCommunityDetailsAsJson(communityId) : null - loading: false - onClicked: { - if (mouse.button === Qt.RightButton) { - // TODO: Implement context menu + currentCurrency: root.rootStore.currencyStore.currentCurrency + getFiatValueFn: (amount, symbol) => root.rootStore.currencyStore.getFiatValue(amount, symbol) + formatCurrencyAmountFn: (amount, symbol, options) => root.rootStore.currencyStore.formatCurrencyAmount(amount, symbol, options) + getNameForAddressFn: (address) => root.walletRootStore.getNameForAddress(address) + getDappDetailsFn: (chainId, address) => root.walletRootStore.getDappDetails(chainId, address) + getTransactionTypeFn: (transaction) => root.walletRootStore.getTransactionType(transaction) + getCommunityDetailsFn: (cid) => root.communitiesStore?.getCommunityDetailsAsJson(cid) + localeUtils: LocaleUtils + } + + StatusListView { + anchors.fill: parent + model: activityModelAdaptor.model + header: ShapeRectangle { + width: parent.width + height: visible ? 42: 0 + visible: !root.activityModel.count && !d.activityLoading + font.pixelSize: Theme.primaryTextFontSize + text: qsTr("Activity will appear here") + } + delegate: TransactionDelegate { + width: ListView.view.width + timeStampText: isModelDataValid ? LocaleUtils.formatRelativeTimestamp(modelData.timestamp * 1000, true) : "" + currentCurrency: root.rootStore.currencyStore.currentCurrency + formatCurrencyAmountFn: (amount, symbol, options) => root.rootStore.currencyStore.formatCurrencyAmount(amount, symbol, options) + showAllAccounts: root.walletRootStore.showAllAccounts + displayValues: true + onClicked: { + if (mouse.button === Qt.RightButton) { + // TODO: Implement context menu + } } } } diff --git a/ui/app/AppLayouts/Browser/stores/BookmarksStore.qml b/ui/app/AppLayouts/stores/Browser/BookmarksStore.qml similarity index 100% rename from ui/app/AppLayouts/Browser/stores/BookmarksStore.qml rename to ui/app/AppLayouts/stores/Browser/BookmarksStore.qml diff --git a/ui/app/AppLayouts/Browser/stores/BrowserActivityStore.qml b/ui/app/AppLayouts/stores/Browser/BrowserActivityStore.qml similarity index 100% rename from ui/app/AppLayouts/Browser/stores/BrowserActivityStore.qml rename to ui/app/AppLayouts/stores/Browser/BrowserActivityStore.qml diff --git a/ui/app/AppLayouts/Browser/stores/BrowserRootStore.qml b/ui/app/AppLayouts/stores/Browser/BrowserRootStore.qml similarity index 100% rename from ui/app/AppLayouts/Browser/stores/BrowserRootStore.qml rename to ui/app/AppLayouts/stores/Browser/BrowserRootStore.qml diff --git a/ui/app/AppLayouts/Browser/stores/BrowserWalletStore.qml b/ui/app/AppLayouts/stores/Browser/BrowserWalletStore.qml similarity index 100% rename from ui/app/AppLayouts/Browser/stores/BrowserWalletStore.qml rename to ui/app/AppLayouts/stores/Browser/BrowserWalletStore.qml diff --git a/ui/app/AppLayouts/Browser/stores/DownloadsStore.qml b/ui/app/AppLayouts/stores/Browser/DownloadsStore.qml similarity index 100% rename from ui/app/AppLayouts/Browser/stores/DownloadsStore.qml rename to ui/app/AppLayouts/stores/Browser/DownloadsStore.qml diff --git a/ui/app/AppLayouts/Browser/stores/qmldir b/ui/app/AppLayouts/stores/Browser/qmldir similarity index 100% rename from ui/app/AppLayouts/Browser/stores/qmldir rename to ui/app/AppLayouts/stores/Browser/qmldir diff --git a/ui/app/AppLayouts/stores/RootStore.qml b/ui/app/AppLayouts/stores/RootStore.qml index 47f79a31689..c00421b8d86 100644 --- a/ui/app/AppLayouts/stores/RootStore.qml +++ b/ui/app/AppLayouts/stores/RootStore.qml @@ -9,6 +9,7 @@ import SortFilterProxyModel import AppLayouts.Profile.stores as ProfileStores import AppLayouts.Wallet.stores as WalletStore import AppLayouts.stores.Messaging as MessagingStores +import AppLayouts.stores.Browser as BrowserStores // WIP: Previous reorganization step before refactoring `RootStore` QtObject { @@ -66,6 +67,14 @@ QtObject { readonly property ContactsStore contactsStore: ContactsStore {} readonly property ActivityCenterStore activityCenterStore: ActivityCenterStore {} + readonly property BrowserStores.BookmarksStore bookmarksStore: BrowserStores.BookmarksStore {} + readonly property BrowserStores.DownloadsStore downloadsStore: BrowserStores.DownloadsStore {} + readonly property BrowserStores.BrowserRootStore browserRootStore: BrowserStores.BrowserRootStore {} + readonly property BrowserStores.BrowserWalletStore browserWalletStore: BrowserStores.BrowserWalletStore {} + readonly property BrowserStores.BrowserActivityStore browserActivityStore: BrowserStores.BrowserActivityStore { + browserWalletStore: root.browserWalletStore + } + // readonly property ChatStores.RootStore rootChatStore: ChatStores.RootStore { ... } // readonly property SharedStores.NetworkConnectionStore networkConnectionStore: SharedStores.NetworkConnectionStore { ... } // + all the rest of stores now created on `AppMain` diff --git a/ui/app/mainui/AppMain.qml b/ui/app/mainui/AppMain.qml index 88200d25cd1..6d72e119e8a 100644 --- a/ui/app/mainui/AppMain.qml +++ b/ui/app/mainui/AppMain.qml @@ -51,7 +51,7 @@ import AppLayouts.Wallet.popups.dapps as DAppsPopups import AppLayouts.Wallet.stores as WalletStores import AppLayouts.stores as AppStores import AppLayouts.stores.Messaging as MessagingStores -import AppLayouts.Browser.stores as BrowserStores +import AppLayouts.stores.Browser as BrowserStores import AppLayouts.ActivityCenter import AppLayouts.ActivityCenter.popups @@ -2089,13 +2089,11 @@ Item { userUID: appMain.profileStore.pubKey thirdpartyServicesEnabled: appMain.rootStore.thirdpartyServicesEnabled navBar: appMain.navBar - bookmarksStore: BrowserStores.BookmarksStore {} - downloadsStore: BrowserStores.DownloadsStore {} - browserRootStore: BrowserStores.BrowserRootStore {} - browserWalletStore: BrowserStores.BrowserWalletStore {} - browserActivityStore: BrowserStores.BrowserActivityStore { - browserWalletStore: browserLayout.browserWalletStore - } + bookmarksStore: appMain.rootStore.bookmarksStore + downloadsStore: appMain.rootStore.downloadsStore + browserRootStore: appMain.rootStore.browserRootStore + browserWalletStore: appMain.rootStore.browserWalletStore + browserActivityStore: appMain.rootStore.browserActivityStore currencyStore: appMain.currencyStore networksStore: appMain.networksStore connectorController: WalletStores.RootStore.dappsConnectorController diff --git a/ui/imports/shared/controls/TransactionDelegate.qml b/ui/imports/shared/controls/TransactionDelegate.qml index 8682fa3bd77..b1d34a59583 100644 --- a/ui/imports/shared/controls/TransactionDelegate.qml +++ b/ui/imports/shared/controls/TransactionDelegate.qml @@ -24,14 +24,16 @@ import shared.stores as SharedStores Delegate to display transaction activity data. \qml + // Normal usage - in ListView with TransactionsModelAdaptor model TransactionDelegate { - id: delegate width: ListView.view.width - modelData: model.activityEntry - flatNetworks: root.flatNetworks - currenciesStore: root.currencyStore - activityStore: root.activityStore - loading: isModelDataValid + currentCurrency: root.currentCurrency + formatCurrencyAmountFn: root.formatCurrencyAmountFn + } + + // Loading skeleton + TransactionDelegate { + loading: true } \endqml @@ -43,126 +45,52 @@ StatusListItem { signal retryClicked() - property var modelData property string timeStampText: isModelDataValid ? LocaleUtils.formatRelativeTimestamp(modelData.timestamp * 1000) : "" property bool showAllAccounts: false property bool displayValues: true - required property var flatNetworks - - required property SharedStores.CurrenciesStore currenciesStore - required property var activityStore - - readonly property bool isModelDataValid: modelData !== undefined && !!modelData - - readonly property string txID: isModelDataValid ? modelData.id : "INVALID" - readonly property int transactionStatus: isModelDataValid ? modelData.status : Constants.TransactionStatus.Pending - readonly property bool isMultiTransaction: isModelDataValid && modelData.isMultiTransaction - readonly property string currentCurrency: currenciesStore.currentCurrency - readonly property double cryptoValue: isModelDataValid ? modelData.amount : 0.0 - readonly property double fiatValue: isModelDataValid ? currenciesStore.getFiatValue(cryptoValue, modelData.symbol) : 0.0 - readonly property double inCryptoValue: isModelDataValid ? modelData.inAmount : 0.0 - readonly property double inFiatValue: isModelDataValid && isMultiTransaction ? currenciesStore.getFiatValue(inCryptoValue, modelData.inSymbol): 0.0 - readonly property double outCryptoValue: isModelDataValid ? modelData.outAmount : 0.0 - readonly property double outFiatValue: isModelDataValid && isMultiTransaction ? currenciesStore.getFiatValue(outCryptoValue, modelData.outSymbol): 0.0 - readonly property string networkColor: isModelDataValid ? SQUtils.ModelUtils.getByKey(flatNetworks, "chainId", modelData.chainId, "chainColor") : "" - readonly property string networkName: isModelDataValid ? SQUtils.ModelUtils.getByKey(flatNetworks, "chainId", modelData.chainId, "chainName") : "" - readonly property string networkNameIn: isMultiTransaction ? SQUtils.ModelUtils.getByKey(flatNetworks, "chainId", modelData.chainIdIn, "chainName") : "" - readonly property string networkNameOut: isMultiTransaction ? SQUtils.ModelUtils.getByKey(flatNetworks, "chainId", modelData.chainIdOut, "chainName") : "" - readonly property string addressNameTo: isModelDataValid ? activityStore.getNameForAddress(modelData.recipient) : "" - readonly property string addressNameFrom: isModelDataValid ? activityStore.getNameForAddress(modelData.sender) : "" - readonly property bool isNFT: isModelDataValid && modelData.isNFT - readonly property bool isCommunityAssetViaAirdrop: isModelDataValid && !!communityId && d.txType === Constants.TransactionType.Mint - readonly property string communityId: isModelDataValid && modelData.communityId ? modelData.communityId : "" - property var community: null - readonly property bool isCommunityToken: !!community && Object.keys(community).length > 0 - readonly property string communityImage: isCommunityToken ? community.image : "" - readonly property string communityName: isCommunityToken ? community.name : "" - - readonly property var dAppDetails: { - if (!isModelDataValid) { - return null - } - if (modelData.txType === Constants.TransactionType.Approve) { - return activityStore.getDappDetails(modelData.chainId, modelData.approvalSpender) - } - if (modelData.txType === Constants.TransactionType.Swap) { - return activityStore.getDappDetails(modelData.chainId, modelData.interactedContractAddress) - } - return null - } - - readonly property string dAppIcon: dAppDetails ? dAppDetails.icon : "" - readonly property string dAppUrl: dAppDetails ? dAppDetails.url : "" - readonly property string dAppName: dAppDetails ? dAppDetails.name : "" - - readonly property string transactionValue: { - if (!isModelDataValid) { - return qsTr("N/A") - } else if (root.isNFT) { - let value = "" - if (d.txType === Constants.TransactionType.Mint) { - value += modelData.amount + " " - } - if (modelData.nftName) { - value += modelData.nftName - } else if (modelData.tokenID) { - value += "#" + modelData.tokenID - } else { - value += qsTr("Unknown NFT") - } - return value - } else if (!modelData.symbol && !!modelData.tokenAddress) { - return "%1 (%2)".arg(root.currenciesStore.formatCurrencyAmount(cryptoValue, "")).arg(Utils.compactAddress(modelData.tokenAddress, 4)) - } - return root.currenciesStore.formatCurrencyAmount(cryptoValue, modelData.symbol) - } - - readonly property string inTransactionValue: { - if (!isModelDataValid) { - return qsTr("N/A") - } else if (!modelData.inSymbol && !!modelData.tokenInAddress) { - return "%1 (%2)".arg(root.currenciesStore.formatCurrencyAmount(inCryptoValue, "")).arg(Utils.compactAddress(modelData.tokenInAddress, 4)) - } - return currenciesStore.formatCurrencyAmount(inCryptoValue, modelData.inSymbol) - } - readonly property string outTransactionValue: { - if (!isModelDataValid) { - return qsTr("N/A") - } else if (!modelData.outSymbol && !!modelData.tokenOutAddress) { - return "%1 (%2)".arg(root.currenciesStore.formatCurrencyAmount(outCryptoValue, "")).arg(Utils.compactAddress(modelData.tokenOutAddress, 4)) - } - return currenciesStore.formatCurrencyAmount(outCryptoValue, modelData.outSymbol) - } - - readonly property string tokenImage: { - if (!isModelDataValid || - d.txType === Constants.TransactionType.ContractDeployment || - d.txType === Constants.TransactionType.ContractInteraction) - return "" - if (root.isNFT) { - return modelData.nftImageUrl ? modelData.nftImageUrl : "" - } else { - return Constants.tokenIcon(isMultiTransaction ? d.txType === Constants.TransactionType.Receive ? modelData.inSymbol : modelData.outSymbol : modelData.symbol) - } - } - - readonly property string inTokenImage: isModelDataValid ? Constants.tokenIcon(modelData.inSymbol) : "" - - readonly property string toAddress: !!addressNameTo ? - addressNameTo : - isModelDataValid ? - Utils.compactAddress(modelData.recipient, 4) : - "" - - readonly property string fromAddress: !!addressNameFrom ? - addressNameFrom : - isModelDataValid ? - Utils.compactAddress(modelData.sender, 4) : - "" - - readonly property string interactedContractAddress: isModelDataValid ? Utils.compactAddress(modelData.interactedContractAddress, 4) : "" - readonly property string approvalSpender: isModelDataValid ? Utils.compactAddress(modelData.approvalSpender, 4) : "" + // for fiat value formatting in UI + property string currentCurrency: "" + property var formatCurrencyAmountFn: function(amount, symbol, options) { return "" } + + // All computed properties come from model (via TransactionsModelAdaptor) + readonly property var modelData: loading ? null : (model?.activityEntry ?? null) + readonly property bool isModelDataValid: !loading && modelData !== null && modelData !== undefined + readonly property string txID: loading ? "" : (model?.txID ?? "") + readonly property int transactionStatus: loading ? Constants.TransactionStatus.Pending : (model?.transactionStatus ?? Constants.TransactionStatus.Pending) + readonly property bool isMultiTransaction: loading ? false : (model?.isMultiTransaction ?? false) + readonly property double cryptoValue: loading ? 0.0 : (model?.cryptoValue ?? 0.0) + readonly property double fiatValue: loading ? 0.0 : (model?.fiatValue ?? 0.0) + readonly property double inCryptoValue: loading ? 0.0 : (model?.inCryptoValue ?? 0.0) + readonly property double inFiatValue: loading ? 0.0 : (model?.inFiatValue ?? 0.0) + readonly property double outCryptoValue: loading ? 0.0 : (model?.outCryptoValue ?? 0.0) + readonly property double outFiatValue: loading ? 0.0 : (model?.outFiatValue ?? 0.0) + readonly property string networkColor: loading ? "" : (model?.networkColor ?? "") + readonly property string networkName: loading ? "" : (model?.networkName ?? "") + readonly property string networkNameIn: loading ? "" : (model?.networkNameIn ?? "") + readonly property string networkNameOut: loading ? "" : (model?.networkNameOut ?? "") + readonly property string addressNameTo: loading ? "" : (model?.addressNameTo ?? "") + readonly property string addressNameFrom: loading ? "" : (model?.addressNameFrom ?? "") + readonly property bool isNFT: loading ? false : (model?.isNFT ?? false) + readonly property bool isCommunityAssetViaAirdrop: loading ? false : (model?.isCommunityAssetViaAirdrop ?? false) + readonly property string communityId: loading ? "" : (model?.communityId ?? "") + readonly property var community: loading ? null : (model?.community ?? null) + readonly property bool isCommunityToken: loading ? false : (model?.isCommunityToken ?? false) + readonly property string communityImage: loading ? "" : (model?.communityImage ?? "") + readonly property string communityName: loading ? "" : (model?.communityName ?? "") + readonly property var dAppDetails: loading ? null : (model?.dAppDetails ?? null) + readonly property string dAppIcon: loading ? "" : (model?.dAppIcon ?? "") + readonly property string dAppUrl: loading ? "" : (model?.dAppUrl ?? "") + readonly property string dAppName: loading ? "" : (model?.dAppName ?? "") + readonly property string transactionValue: loading ? "" : (model?.transactionValue ?? "") + readonly property string inTransactionValue: loading ? "" : (model?.inTransactionValue ?? "") + readonly property string outTransactionValue: loading ? "" : (model?.outTransactionValue ?? "") + readonly property string tokenImage: loading ? "" : (model?.tokenImage ?? "") + readonly property string inTokenImage: loading ? "" : (model?.inTokenImage ?? "") + readonly property string toAddress: loading ? "" : (model?.toAddress ?? "") + readonly property string fromAddress: loading ? "" : (model?.fromAddress ?? "") + readonly property string interactedContractAddress: loading ? "" : (model?.interactedContractAddress ?? "") + readonly property string approvalSpender: loading ? "" : (model?.approvalSpender ?? "") property StatusAssetSettings statusIconAsset: StatusAssetSettings { width: 12 @@ -212,7 +140,7 @@ StatusListItem { readonly property bool isLightTheme: Theme.palette.name === Constants.lightThemeName property color animatedBgColor - readonly property int txType: activityStore.getTransactionType(root.modelData) + readonly property int txType: root.loading ? 0 : (model?.txType ?? 0) readonly property var secondIconAsset: StatusAssetSettings { width: root.tokenIconAsset.width @@ -320,8 +248,7 @@ StatusListItem { } rightPadding: 16 - enabled: !loading - loading: !isModelDataValid + enabled: !root.loading color: { if (bgColorAnimation.running) { return d.animatedBgColor @@ -532,12 +459,12 @@ StatusListItem { switch(d.txType) { case Constants.TransactionType.Send: - return "−" + root.currenciesStore.formatCurrencyAmount(root.fiatValue, root.currentCurrency) + return "−" + root.formatCurrencyAmountFn(root.fiatValue, root.currentCurrency) case Constants.TransactionType.Receive: - return "+" + root.currenciesStore.formatCurrencyAmount(root.fiatValue, root.currentCurrency) + return "+" + root.formatCurrencyAmountFn(root.fiatValue, root.currentCurrency) case Constants.TransactionType.Swap: - return "-%1 / +%2".arg(root.currenciesStore.formatCurrencyAmount(root.outFiatValue, root.currentCurrency)) - .arg(root.currenciesStore.formatCurrencyAmount(root.inFiatValue, root.currentCurrency)) + return "-%1 / +%2".arg(root.formatCurrencyAmountFn(root.outFiatValue, root.currentCurrency)) + .arg(root.formatCurrencyAmountFn(root.inFiatValue, root.currentCurrency)) case Constants.TransactionType.Bridge: case Constants.TransactionType.Approve: default: diff --git a/ui/imports/shared/views/HistoryView.qml b/ui/imports/shared/views/HistoryView.qml index 6a2cfafb8a2..0f0de3726f1 100644 --- a/ui/imports/shared/views/HistoryView.qml +++ b/ui/imports/shared/views/HistoryView.qml @@ -31,17 +31,40 @@ ColumnLayout { property var overview - property var activityStore - property CommunitiesStore communitiesStore - property SharedStores.CurrenciesStore currencyStore - required property SharedStores.NetworksStore networksStore + property bool loadingHistoryTransactions: false + property var historyTransactionsModel: null + property bool newDataAvailable: false + property bool isNonArchivalNode: false + property string selectedAddress: "" property bool showAllAccounts: false + property bool isFilterDirty: false + property var activeNetworks: null + property var allNetworks: null + property string currentCurrency: "" + + property var getNameForAddressFn: function(address) { return "" } + property var getDappDetailsFn: function(chainId, address) { return null } + property var getFiatValueFn: function(amount, symbol) { return 0.0 } + property var formatCurrencyAmountFn: function(amount, symbol, options) { return "" } + property var getTransactionTypeFn: function(transaction) { return 0 } + + property CommunitiesStore communitiesStore: null + + property var activityStore: null + property bool displayValues: true property bool filterVisible property bool disableShadowOnScroll: false property bool hideVerticalScrollbar: false property int firstItemOffset: 0 + signal updateTransactionFilterRequested() + signal moreTransactionsRequested() + signal activityDataResetRequested() + signal collectiblesModelUpdateRequested() + signal recipientsModelUpdateRequested() + signal allFiltersApplyRequested() + // banner component to be displayed on top of the list property alias bannerComponent: banner.sourceComponent @@ -54,29 +77,17 @@ ColumnLayout { } Component.onCompleted: { - if (root.activityStore.transactionActivityStatus.isFilterDirty) { - root.activityStore.currentActivityFiltersStore.applyAllFilters() + if (root.isFilterDirty) { + root.allFiltersApplyRequested() } - root.activityStore.currentActivityFiltersStore.updateCollectiblesModel() - root.activityStore.currentActivityFiltersStore.updateRecipientsModel() - } - - Connections { - target: root.activityStore.transactionActivityStatus - enabled: root.visible - function onIsFilterDirtyChanged() { - root.activityStore.updateTransactionFilterIfDirty() - } - function onFilterChainsChanged() { - root.activityStore.currentActivityFiltersStore.updateCollectiblesModel() - root.activityStore.currentActivityFiltersStore.updateRecipientsModel() - } + root.collectiblesModelUpdateRequested() + root.recipientsModelUpdateRequested() } QtObject { id: d - readonly property bool isInitialLoading: root.activityStore.loadingHistoryTransactions && transactionListRoot.count === 0 + readonly property bool isInitialLoading: root.loadingHistoryTransactions && transactionListRoot.count === 0 readonly property int loadingSectionWidth: 56 @@ -87,7 +98,7 @@ ColumnLayout { HistoryBetaTag { id: betaTag - flatNetworks: root.networksStore.activeNetworks + flatNetworks: root.activeNetworks Layout.fillWidth: true Layout.alignment: Qt.AlignTop @@ -96,8 +107,8 @@ ColumnLayout { visible: root.firstItemOffset === 0 // visible only in the main wallet view onLinkActivated: { - const explorerUrl = root.activityStore.showAllAccounts ? link - : "%1/%2/%3".arg(link).arg(Constants.networkExplorerLinks.addressPath).arg(root.activityStore.selectedAddress) + const explorerUrl = root.showAllAccounts ? link + : "%1/%2/%3".arg(link).arg(Constants.networkExplorerLinks.addressPath).arg(root.selectedAddress) Global.requestOpenLink(explorerUrl) } } @@ -107,7 +118,7 @@ ColumnLayout { Layout.fillWidth: true Layout.alignment: Qt.AlignTop Layout.topMargin: root.firstItemOffset - visible: root.activityStore.isNonArchivalNode + visible: root.isNonArchivalNode text: qsTr("Status Desktop is connected to a non-archival node. Transaction history may be incomplete.") font.pixelSize: Theme.primaryTextFontSize color: Theme.palette.dangerColor1 @@ -116,7 +127,7 @@ ColumnLayout { Loader { id: filterPanelLoader - active: root.filterVisible && (d.isInitialLoading || transactionListRoot.count > 0 || root.activityStore.currentActivityFiltersStore.filtersSet) + active: root.filterVisible && (d.isInitialLoading || transactionListRoot.count > 0 || root.activityStore?.currentActivityFiltersStore?.filtersSet) visible: active && !noTxs.visible Layout.fillWidth: true sourceComponent: ActivityFilterPanel { @@ -137,7 +148,7 @@ ColumnLayout { Layout.fillWidth: true Layout.preferredHeight: 42 Layout.topMargin: !nonArchivalNodeError.visible? root.firstItemOffset : 0 - visible: !d.isInitialLoading && !root.activityStore.currentActivityFiltersStore.filtersSet && transactionListRoot.count === 0 + visible: !d.isInitialLoading && !root.activityStore?.currentActivityFiltersStore?.filtersSet && transactionListRoot.count === 0 font.pixelSize: Theme.primaryTextFontSize text: qsTr("Activity for this account will appear here") } @@ -157,54 +168,26 @@ ColumnLayout { visible: !root.disableShadowOnScroll && !transactionListRoot.atYBeginning } + TransactionsModelAdaptor { + id: txModelAdaptor + sourceModel: root.historyTransactionsModel + flatNetworks: root.allNetworks + currentCurrency: root.currentCurrency + getFiatValueFn: root.getFiatValueFn + formatCurrencyAmountFn: root.formatCurrencyAmountFn + getNameForAddressFn: root.getNameForAddressFn + getDappDetailsFn: root.getDappDetailsFn + getTransactionTypeFn: root.getTransactionTypeFn + getCommunityDetailsFn: (cid) => root.communitiesStore?.getCommunityDetailsAsJson(cid) + localeUtils: LocaleUtils + } + StatusListView { id: transactionListRoot objectName: "walletAccountTransactionList" anchors.fill: parent - model: SortFilterProxyModel { - id: txModel - - sourceModel: root.activityStore.historyTransactions - - // LocaleUtils is not accessable from inside expression, but local function works - property var daysTo: (d1, d2) => LocaleUtils.daysTo(d1, d2) - property var daysBetween: (d1, d2) => LocaleUtils.daysBetween(d1, d2) - property var getFirstDayOfTheCurrentWeek: () => LocaleUtils.getFirstDayOfTheCurrentWeek() - proxyRoles: ExpressionRole { - name: "date" - expression: { - if (!model.activityEntry || model.activityEntry.timestamp === 0) - return "" - const currDate = new Date() - const timestampDate = new Date(model.activityEntry.timestamp * 1000) - const daysDiff = txModel.daysBetween(currDate, timestampDate) - const daysToBeginingOfThisWeek = txModel.daysTo(timestampDate, txModel.getFirstDayOfTheCurrentWeek()) - - if (daysDiff < 1) { - return qsTr("Today") - } else if (daysDiff < 2) { - return qsTr("Yesterday") - } else if (daysToBeginingOfThisWeek >= 0) { - return qsTr("Earlier this week") - } else if (daysToBeginingOfThisWeek > -7) { - return qsTr("Last week") - } else if (currDate.getMonth() === timestampDate.getMonth() && currDate.getYear() === timestampDate.getYear()) { - return qsTr("Earlier this month") - } - - const previousMonthDate = (new Date(new Date().setDate(0))) - // Special case for the end of the year - if ((timestampDate.getMonth() === previousMonthDate.getMonth() && timestampDate.getYear() === previousMonthDate.getYear()) - || (previousMonthDate.getMonth() === 11 && timestampDate.getMonth() === 0 && Math.abs(timestampDate.getYear() - previousMonthDate.getYear()) === 1)) - { - return qsTr("Last month") - } - - return timestampDate.toLocaleDateString(Qt.locale(), "MMM yyyy") - } - } - } + model: txModelAdaptor.model delegate: transactionDelegateComponent @@ -220,17 +203,17 @@ ColumnLayout { } Connections { - target: root.activityStore + target: root function onLoadingHistoryTransactionsChanged() { // Calling timer instead directly to not cause binding loop - if (!root.activityStore.loadingHistoryTransactions) + if (!root.loadingHistoryTransactions) fetchMoreTimer.start() } } function tryFetchMoreTransactions() { - if (d.isInitialLoading || !footerItem || !root.activityStore.historyTransactions.hasMore) + if (d.isInitialLoading || !footerItem || !root.historyTransactionsModel?.hasMore) return const footerYPosition = footerItem.height / contentHeight if (footerYPosition >= 1.0) { @@ -239,7 +222,7 @@ ColumnLayout { // On startup, first loaded ListView will have heightRatio equal 0 if (footerYPosition + visibleArea.yPosition + visibleArea.heightRatio > 1.0) { - root.activityStore.fetchMoreTransactions() + root.moreTransactionsRequested() } } @@ -258,8 +241,8 @@ ColumnLayout { text: qsTr("New transactions") - visible: root.activityStore.newDataAvailable && !root.activityStore.loadingHistoryTransactions - onClicked: root.activityStore.resetActivityData() + visible: root.newDataAvailable && !root.loadingHistoryTransactions + onClicked: root.activityDataResetRequested() icon.name: "arrow-up" @@ -303,8 +286,8 @@ ColumnLayout { required property var model required property int index - readonly property bool displaySectionHeader: index === 0 || model.date !== txModel.get(index - 1).date - readonly property bool displaySectionFooter: index === txModel.count-1 || model.date !== txModel.get(index + 1).date + readonly property bool displaySectionHeader: index === 0 || model.date !== SQUtils.ModelUtils.get(txModelAdaptor.model, index - 1, "date") + readonly property bool displaySectionFooter: index === txModelAdaptor.model.rowCount() - 1 || model.date !== SQUtils.ModelUtils.get(txModelAdaptor.model, index + 1, "date") width: ListView.view.width spacing: 0 @@ -343,14 +326,11 @@ ColumnLayout { TransactionDelegate { Layout.fillWidth: true - modelData: transactionDelegate.model.activityEntry timeStampText: isModelDataValid ? LocaleUtils.formatRelativeTimestamp(modelData.timestamp * 1000, true) : "" - flatNetworks: root.networksStore.allNetworks - currenciesStore: root.currencyStore - activityStore: root.activityStore + currentCurrency: root.currentCurrency + formatCurrencyAmountFn: root.formatCurrencyAmountFn showAllAccounts: root.showAllAccounts displayValues: root.displayValues - community: isModelDataValid && !!communityId && !!root.communitiesStore ? root.communitiesStore.getCommunityDetailsAsJson(communityId) : null onClicked: function(itemId, mouse) { if (mouse.button === Qt.RightButton) { txContextMenu.createObject(this, { modelData }).popup(mouse.x, mouse.y) @@ -371,7 +351,7 @@ ColumnLayout { id: footerComp ColumnLayout { id: footerColumn - readonly property bool allActivityLoaded: !root.activityStore.historyTransactions.hasMore && transactionListRoot.count !== 0 + readonly property bool allActivityLoaded: !root.historyTransactionsModel?.hasMore && transactionListRoot.count !== 0 width: root.width spacing: d.isInitialLoading ? 6 : 12 @@ -399,7 +379,7 @@ ColumnLayout { const delegateHeight = 64 + footerColumn.spacing if (d.isInitialLoading) { return Math.floor(transactionListRoot.height / delegateHeight) - } else if (root.activityStore.historyTransactions.hasMore) { + } else if (root.historyTransactionsModel?.hasMore) { return Math.max(3, Math.floor(transactionListRoot.height / delegateHeight) - 3) } } @@ -407,10 +387,6 @@ ColumnLayout { } TransactionDelegate { Layout.fillWidth: true - - flatNetworks: root.networksStore.allNetworks - currenciesStore: root.currencyStore - activityStore: root.activityStore loading: true } } diff --git a/ui/imports/shared/views/TransactionsModelAdaptor.qml b/ui/imports/shared/views/TransactionsModelAdaptor.qml new file mode 100644 index 00000000000..519ece1f6db --- /dev/null +++ b/ui/imports/shared/views/TransactionsModelAdaptor.qml @@ -0,0 +1,188 @@ +import QtQml + +import StatusQ +import StatusQ.Core.Utils + +import utils + +import QtModelsToolkit + +/*! + \qmltype TransactionsModelAdaptor + \inqmlmodule shared.adaptors + \since shared.adaptors 1.0 + \brief Model adaptor that computes derived transaction properties + + Wraps a transactions model with ObjectProxyModel to compute all derived + properties at the model layer instead of per-delegate at render time. + + Includes date grouping computation (Today, Yesterday, etc.) that was + previously in HistoryView's SortFilterProxyModel. +*/ + +QObject { + id: root + + required property var sourceModel + required property var flatNetworks + required property string currentCurrency + required property var getFiatValueFn + required property var formatCurrencyAmountFn + required property var getNameForAddressFn + required property var getDappDetailsFn + required property var getTransactionTypeFn + property var getCommunityDetailsFn: null + required property var localeUtils + + readonly property alias model: objectProxyModel + + ObjectProxyModel { + id: objectProxyModel + sourceModel: root.sourceModel + + delegate: QtObject { + readonly property var activityEntry: model.activityEntry + readonly property var modelData: model.activityEntry + readonly property bool isModelDataValid: modelData !== undefined && !!modelData + + readonly property string date: { + if (!isModelDataValid || modelData.timestamp === 0) + return "" + + const currDate = new Date() + const timestampDate = new Date(modelData.timestamp * 1000) + const daysDiff = root.localeUtils.daysBetween(currDate, timestampDate) + const daysToBeginingOfThisWeek = root.localeUtils.daysTo(timestampDate, root.localeUtils.getFirstDayOfTheCurrentWeek()) + + if (daysDiff < 1) + return qsTr("Today") + if (daysDiff < 2) + return qsTr("Yesterday") + if (daysToBeginingOfThisWeek >= 0) + return qsTr("Earlier this week") + if (daysToBeginingOfThisWeek > -7) + return qsTr("Last week") + if (currDate.getMonth() === timestampDate.getMonth() && + currDate.getYear() === timestampDate.getYear()) + return qsTr("Earlier this month") + + const previousMonthDate = new Date(new Date().setDate(0)) + if ((timestampDate.getMonth() === previousMonthDate.getMonth() && + timestampDate.getYear() === previousMonthDate.getYear()) || + (previousMonthDate.getMonth() === 11 && timestampDate.getMonth() === 0 && + Math.abs(timestampDate.getYear() - previousMonthDate.getYear()) === 1)) + return qsTr("Last month") + + return timestampDate.toLocaleDateString(Qt.locale(), "MMM yyyy") + } + + readonly property string txID: isModelDataValid ? modelData.id : "INVALID" + readonly property int transactionStatus: isModelDataValid ? modelData.status : Constants.TransactionStatus.Pending + readonly property bool isMultiTransaction: isModelDataValid && modelData.isMultiTransaction + readonly property double cryptoValue: isModelDataValid ? modelData.amount : 0.0 + readonly property double fiatValue: isModelDataValid ? root.getFiatValueFn(cryptoValue, modelData.symbol) : 0.0 + readonly property double inCryptoValue: isModelDataValid ? modelData.inAmount : 0.0 + readonly property double inFiatValue: isModelDataValid && isMultiTransaction ? root.getFiatValueFn(inCryptoValue, modelData.inSymbol) : 0.0 + readonly property double outCryptoValue: isModelDataValid ? modelData.outAmount : 0.0 + readonly property double outFiatValue: isModelDataValid && isMultiTransaction ? root.getFiatValueFn(outCryptoValue, modelData.outSymbol) : 0.0 + + readonly property string networkColor: isModelDataValid ? ModelUtils.getByKey(root.flatNetworks, "chainId", modelData.chainId, "chainColor") : "" + readonly property string networkName: isModelDataValid ? ModelUtils.getByKey(root.flatNetworks, "chainId", modelData.chainId, "chainName") : "" + readonly property string networkNameIn: isMultiTransaction ? ModelUtils.getByKey(root.flatNetworks, "chainId", modelData.chainIdIn, "chainName") : "" + readonly property string networkNameOut: isMultiTransaction ? ModelUtils.getByKey(root.flatNetworks, "chainId", modelData.chainIdOut, "chainName") : "" + + readonly property string addressNameTo: isModelDataValid ? root.getNameForAddressFn(modelData.recipient) : "" + readonly property string addressNameFrom: isModelDataValid ? root.getNameForAddressFn(modelData.sender) : "" + + readonly property bool isNFT: isModelDataValid && modelData.isNFT + readonly property string communityId: isModelDataValid && modelData.communityId ? modelData.communityId : "" + readonly property var community: root.getCommunityDetailsFn && !!communityId ? root.getCommunityDetailsFn(communityId) : null + readonly property bool isCommunityToken: !!community && Object.keys(community).length > 0 + readonly property string communityImage: isCommunityToken ? community.image : "" + readonly property string communityName: isCommunityToken ? community.name : "" + + readonly property int txType: root.getTransactionTypeFn(modelData) + readonly property bool isCommunityAssetViaAirdrop: isModelDataValid && !!communityId && txType === Constants.TransactionType.Mint + + readonly property var dAppDetails: { + if (!isModelDataValid) + return null + if (modelData.txType === Constants.TransactionType.Approve) + return root.getDappDetailsFn(modelData.chainId, modelData.approvalSpender) + if (modelData.txType === Constants.TransactionType.Swap) + return root.getDappDetailsFn(modelData.chainId, modelData.interactedContractAddress) + return null + } + readonly property string dAppIcon: dAppDetails ? dAppDetails.icon : "" + readonly property string dAppUrl: dAppDetails ? dAppDetails.url : "" + readonly property string dAppName: dAppDetails ? dAppDetails.name : "" + + readonly property string transactionValue: { + if (!isModelDataValid) + return qsTr("N/A") + if (isNFT) { + let value = "" + if (txType === Constants.TransactionType.Mint) + value += modelData.amount + " " + if (modelData.nftName) + value += modelData.nftName + else if (modelData.tokenID) + value += "#" + modelData.tokenID + else + value += qsTr("Unknown NFT") + return value + } + if (!modelData.symbol && !!modelData.tokenAddress) + return "%1 (%2)".arg(root.formatCurrencyAmountFn(cryptoValue, "")).arg(Utils.compactAddress(modelData.tokenAddress, 4)) + return root.formatCurrencyAmountFn(cryptoValue, modelData.symbol) + } + + readonly property string inTransactionValue: { + if (!isModelDataValid) + return qsTr("N/A") + if (!modelData.inSymbol && !!modelData.tokenInAddress) + return "%1 (%2)".arg(root.formatCurrencyAmountFn(inCryptoValue, "")).arg(Utils.compactAddress(modelData.tokenInAddress, 4)) + return root.formatCurrencyAmountFn(inCryptoValue, modelData.inSymbol) + } + + readonly property string outTransactionValue: { + if (!isModelDataValid) + return qsTr("N/A") + if (!modelData.outSymbol && !!modelData.tokenOutAddress) + return "%1 (%2)".arg(root.formatCurrencyAmountFn(outCryptoValue, "")).arg(Utils.compactAddress(modelData.tokenOutAddress, 4)) + return root.formatCurrencyAmountFn(outCryptoValue, modelData.outSymbol) + } + + readonly property string tokenImage: { + if (!isModelDataValid || + txType === Constants.TransactionType.ContractDeployment || + txType === Constants.TransactionType.ContractInteraction) + return "" + if (isNFT) + return modelData.nftImageUrl ? modelData.nftImageUrl : "" + return Constants.tokenIcon(isMultiTransaction ? txType === Constants.TransactionType.Receive ? modelData.inSymbol : modelData.outSymbol : modelData.symbol) + } + + readonly property string inTokenImage: isModelDataValid ? Constants.tokenIcon(modelData.inSymbol) : "" + + readonly property string toAddress: !!addressNameTo ? addressNameTo : isModelDataValid ? Utils.compactAddress(modelData.recipient, 4) : "" + readonly property string fromAddress: !!addressNameFrom ? addressNameFrom : isModelDataValid ? Utils.compactAddress(modelData.sender, 4) : "" + + readonly property string interactedContractAddress: isModelDataValid ? Utils.compactAddress(modelData.interactedContractAddress, 4) : "" + readonly property string approvalSpender: isModelDataValid ? Utils.compactAddress(modelData.approvalSpender, 4) : "" + } + + expectedRoles: ["activityEntry"] + exposedRoles: [ + "activityEntry", "date", "isModelDataValid", "txID", "transactionStatus", + "isMultiTransaction", "cryptoValue", "fiatValue", "inCryptoValue", "inFiatValue", + "outCryptoValue", "outFiatValue", "networkColor", "networkName", "networkNameIn", + "networkNameOut", "addressNameTo", "addressNameFrom", "isNFT", + "isCommunityAssetViaAirdrop", "communityId", "community", "isCommunityToken", + "communityImage", "communityName", "txType", "dAppDetails", "dAppIcon", "dAppUrl", + "dAppName", "transactionValue", "inTransactionValue", "outTransactionValue", + "tokenImage", "inTokenImage", "toAddress", "fromAddress", + "interactedContractAddress", "approvalSpender" + ] + } +} diff --git a/ui/imports/shared/views/qmldir b/ui/imports/shared/views/qmldir index 09b0b0d6699..b36307eccd9 100644 --- a/ui/imports/shared/views/qmldir +++ b/ui/imports/shared/views/qmldir @@ -18,3 +18,4 @@ SyncingEnterCode 1.0 SyncingEnterCode.qml SyncingErrorMessage 1.0 SyncingErrorMessage.qml TransactionSigner 1.0 TransactionSigner.qml BiometricsSectionView 1.0 BiometricsSectionView.qml +TransactionsModelAdaptor 1.0 TransactionsModelAdaptor.qml