Files
status-app/ui/app/AppLayouts/Wallet/views/RightTabView.qml
T
Alex Jbanca 754276d853 perf(wallet): build the wallet section around what the user is waiting for
A wallet section activation built everything it would ever need before it
showed anything: both panels inline, both detail views, every row of two
lists synchronously, and a set of subtrees that exist for cases that almost
never occur. This reorders that around a single rule - build what is on
screen first, build the rest in the incubation controller's metered bites,
and build the rest of the rest only when something asks for it.

Lists fill preemptibly. A ListView refill is one uninterruptible call inside
the window's polish phase, and QQuickItemView creates visible delegates with
AsynchronousIfNested - so a list first laid out after its enclosing Loader is
already Ready builds every visible row synchronously. On a warm load that was
one 34.7ms GUI-thread block against a 3.2ms budget for the assets list, and
8.7-13.5ms of post-content polish across both lists. The assets and accounts
delegates become TokenDelegateShell and WalletAccountDelegateShell: an Item
carrying the row's geometry and its placeholder tiles, with the real row
behind a nested asynchronous Loader. The refill builds shells; the rows
incubate afterwards.

Row height is the shell's, not the content's. StatusListItem.implicitHeight
is Math.max(64, titleArea.height + 16), and both a token row and an account
row resolve to that 64px floor, which is what placeholderHeight is.
tst_TokenDelegateShell and tst_WalletAccountDelegateShell gate the two
staying equal, so a change that makes a row taller fails a test rather than
moving contentHeight and the scroll position mid-fill. Each placeholder
mirrors the skeleton the per-tab loader showed a moment earlier, so the two
loading states read as one handoff.

Subtrees built for the exceptional case are latched off. The token row's two
warning buttons, the asset detail's four chain-tag warning buttons, its
header community tag and its "Minted by" tile were all built unconditionally
and merely hidden. Each now sits behind the condition that used to drive
`visible`. AssetsDetailsHeader loses its `communityTag` alias, which existed
only so callers could reach in and set the tag's contents; it takes
communityName/communityImage and builds the tag itself.

The two detail views move behind async Loaders keyed on the stack index. The
token to show becomes state held in `d` and bound into the view rather than
assigned into it, so a second click landing mid-incubation wins without a
queue-and-replay; the resets that hung off onVisibleChanged hang off
onActiveChanged, which fires on the same edges.

Panels build in the order the user sees them. WalletLayout declared both
panels as plain object bindings, so activation built LeftTabView and the
centre StackView inline and the chrome's two PanelSwapGates could not fire
independently - they opened 5ms apart because both panel properties resolve
when WalletLayout completes. Each panel now sits behind its own async Loader
with its own readiness, the primary one first, and once a skeleton is on
screen with no panel switch running the primary panel is built synchronously:
the pacing exists to protect section-switch animations, and here the only
thing a synchronous block could stutter is the skeleton it replaces.

Wrapping the panels in Loaders exposed three geometry defects. A Loader
reports its item's implicit size as its own, so the centre StackView's
content width propagated into the chrome and pushed the panel past the right
edge on a narrow screen; the loaders are bound to the chrome's geometry for
the unparented phase, the stack no longer anchors to its loader, and the side
inset moves onto the content where it belongs.

Panel geometry is coalesced across a rotation. A device rotation walks the
window through nine sizes over ~430ms and LayoutItemProxy forwarded every one
to the section's panels - a full relayout of a populated subtree each time -
and parked a panel at a degenerate box for ~148ms on the portrait/landscape
handoff. SectionPanelSlot replaces the panel proxies in both sub-layouts and
publishes the box each slot will give its panel, which is also what a section
building panels outside the tree needs: the centre slot cannot be guessed, as
it is short by the header and footer and narrow by the left column in
landscape. Wallet and chat both pre-size their incubating panels to it.

Two fixes fall out of that work. BaseProxyPanel looked its SwipeView page up
at a fixed implicitIndex, but SwipeView indices close up as pages come and
go, so hiding the right panel with no left panel present silently left its
page in the view, still swipeable; it now looks the page up by identity.
SectionPanelSlot also has to be listed in statusq.qrc - without it the type
is absent from the binary's resources, StatusSectionLayout becomes
unavailable and the UI process dies precompiling AppMain.

Measured on a whale profile, Release, arms alternated between rounds:

  warm t_first_asset_row       119-159ms -> 51-74ms
  warm max_stall_ms             31.7-42.6 -> 17.2-26.3ms
  section ready                     631ms -> 207ms
  visible panel promoted            609ms -> 338ms
  cold: skeleton shown -> visible  2272ms -> 1836ms
  objects_total (cold)               4602 -> 3344   (-27%)
  objects_settled                    9569 -> 8297   (-13%)
  token row QObjects                  326 -> 279
  panel geometry distinct sizes    24 -> 5 centre, 19 -> 2 left
  degenerate 0x0 box written    12 occurrences -> 0

Device numbers are only comparable once the app has settled; a run taken
while startup backend work is still in flight measured 925ms for a path that
measures ~520ms settled.

Measurements come from an offscreen storybook wallet bench that is not part
of this PR; see branch `feat/storybook-wallet-loader`.
2026-08-23 09:40:36 +03:00

752 lines
34 KiB
QML

import QtCore
import QtQuick
import QtQuick.Layouts
import StatusQ.Components
import StatusQ.Controls
import StatusQ.Core
import StatusQ.Core.Theme
import StatusQ.Core.Utils as SQUtils
import StatusQ.Popups.Dialog
import AppLayouts.Communities.stores
import AppLayouts.Profile.stores as ProfileStores
import AppLayouts.Wallet.controls
import AppLayouts.Wallet.stores as WalletStores
import AppLayouts.stores as AppLayoutsStores
import shared.controls
import shared.panels
import shared.stores as SharedStores
import shared.views
import utils
import QtModelsToolkit
import SortFilterProxyModel
import "./"
import "../stores"
import "../panels"
import "../views/collectibles"
RightTabBaseView {
id: root
enum TabIndex {
Assets = 0,
Collectibles = 1,
Activity = 2
}
property alias currentTabIndex: walletTabBar.currentIndex
property WalletStores.RootStore walletRootStore
property SharedStores.RootStore sharedRootStore
property AppLayoutsStores.RootStore store
property AppLayoutsStores.ContactsStore contactsStore
property CommunitiesStore communitiesStore
property SharedStores.NetworkConnectionStore networkConnectionStore
required property SharedStores.NetworksStore networksStore
property bool swapEnabled
property bool buyEnabled
property bool dAppsEnabled
property bool dAppsVisible
property var dAppsModel
signal launchShareAddressModal()
signal launchBuyCryptoModal()
signal launchSwapModal(string groupKey)
signal sendTokenRequested(string senderAddress, string groupKey, int tokenType)
signal manageNetworksRequested()
signal dappListRequested()
signal dappConnectRequested()
signal dappDisconnectRequested(string dappUrl)
onManageNetworksRequested: {
Global.changeAppSectionBySectionType(Constants.appSection.profile,
Constants.settingsSubsection.wallet,
Constants.walletSettingsSubsection.manageNetworks)
}
function resetView() {
resetStack()
root.currentTabIndex = 0
}
function resetStack() {
stack.currentIndex = 0;
RootStore.backButtonName = d.getBackButtonText(stack.currentIndex);
}
WalletAccountHeader {
id: header
readonly property var overview: root.walletRootStore.overview
allAccounts: overview.isAllAccounts
emojiId: SQUtils.Emoji.iconId(overview.emoji ?? "")
balance: LocaleUtils.currencyAmountToLocaleString(overview.currencyBalance)
balanceLoading: overview.balanceLoading
color: Utils.getColorForId(Theme.palette, overview.colorId)
name: overview.name
balanceAvailable: !root.networkConnectionStore.accountBalanceNotAvailable
networksModel: root.networksStore.activeNetworks
ensOrElidedAddress: RootStore.overview.ens ||
SQUtils.Utils.elideAndFormatWalletAddress(
RootStore.overview.mixedcaseAddress)
lastReloadedTime: !!root.walletRootStore.lastReloadTimestamp ?
LocaleUtils.formatRelativeTimestamp(
root.walletRootStore.lastReloadTimestamp * 1000) : ""
tokensLoading: root.walletRootStore.isAccountTokensReloading
function hasUnseenNewChains(seenChainsJson) {
const newChains = Constants.chains.newChains
const seen = JSON.parse(seenChainsJson)
for (let i = 0; i < newChains.length; i++)
if (seen.indexOf(newChains[i]) === -1)
return true
return false
}
function markNewChainsSeen(seenChainsJson) {
const seen = JSON.parse(seenChainsJson)
return JSON.stringify([...seen, ...Constants.chains.newChains])
}
showNetworksNotificationIcon: header.hasUnseenNewChains(localAppSettings.seenNetworkChains)
showManageNetworksNotificationIcon: header.hasUnseenNewChains(localAppSettings.seenManageNetworksChains)
FunctionAggregator {
id: chainIdsAggregator
model: SortFilterProxyModel {
sourceModel: root.networksStore.activeNetworks
filters: ValueFilter {
roleName: "isEnabled"
value: true
}
}
initialValue: []
roleName: "chainId"
aggregateFunction: (aggr, value) => [...aggr, value]
}
Binding on networksSelection {
value: chainIdsAggregator.value
}
dAppsEnabled: root.dAppsEnabled
dAppsVisible: root.dAppsVisible
dAppsModel: root.dAppsModel
onDappListRequested: root.dappListRequested()
onDappConnectRequested: root.dappConnectRequested()
onDappDisconnectRequested: (dappUrl) =>root.dappDisconnectRequested(dappUrl)
onManageNetworksRequested: {
if (showManageNetworksNotificationIcon)
localAppSettings.seenManageNetworksChains = header.markNewChainsSeen(localAppSettings.seenManageNetworksChains)
root.manageNetworksRequested()
}
onAddressClicked: root.launchShareAddressModal()
onToggleNetworkRequested: chainId => root.networksStore.toggleNetworkEnabled(chainId)
onNetworksShown: {
if (showNetworksNotificationIcon)
localAppSettings.seenNetworkChains = header.markNewChainsSeen(localAppSettings.seenNetworkChains)
}
onReloadRequested: root.walletRootStore.reloadAccountTokens()
}
header: stack.currentIndex === 0 ? header : null
StackLayout {
id: stack
onCurrentIndexChanged: {
RootStore.backButtonName = d.getBackButtonText(currentIndex)
}
QtObject {
id: d
function getBackButtonText(index) {
switch(index) {
case 1:
return collectiblesString
case 2:
return assetsString
case 3:
return historyString
default:
return ""
}
}
readonly property string collectiblesString: qsTr("Collectibles")
readonly property string assetsString: qsTr("Assets")
readonly property string historyString: qsTr("History")
readonly property var walletViewsMap: [
assetsView,
collectiblesView,
historyView
]
readonly property var detailedCollectibleActivityController: RootStore.tmpActivityController0
// Held here rather than assigned into the view: the detail views are
// deferred, so the token to show is picked before anything to show it
// in exists.
property var assetDetailTokenGroup: null
}
Settings {
id: walletSettings
category: "walletSettings-" + userProfile.pubKey
property real collectiblesViewCustomOrderApplyTimestamp: 0
property bool buyBannerEnabled: true
property bool receiveBannerEnabled: true
}
Component {
id: buyReceiveBannerComponent
BuyReceiveBanner {
id: banner
topPadding: anyVisibleItems ? 8 : 0
bottomPadding: anyVisibleItems ? 20 : 0
onBuyClicked: root.launchBuyCryptoModal()
onReceiveClicked: root.launchShareAddressModal()
buyEnabled: walletSettings.buyBannerEnabled && root.buyEnabled
receiveEnabled: walletSettings.receiveBannerEnabled
onCloseBuy: walletSettings.buyBannerEnabled = false
onCloseReceive: walletSettings.receiveBannerEnabled = false
}
}
Component {
id: confirmHideCommunityAssetsPopup
ConfirmHideCommunityAssetsPopup {
destroyOnClose: true
required property string communityId
onConfirmButtonClicked: {
RootStore.walletAssetsStore.assetsController.showHideGroup(communityId, false /*hide*/)
close();
}
}
}
// StackLayout.currentIndex === 0
ColumnLayout {
spacing: 0
ImportKeypairInfo {
Layout.fillWidth: true
Layout.topMargin: Theme.bigPadding
Layout.preferredHeight: childrenRect.height
visible: root.walletRootStore.walletSectionInst.hasPairedDevices
&& root.walletRootStore.walletSectionInst.keypairOperabilityForObservedAccount === Constants.keypair.operability.nonOperable
onRunImport: {
root.walletRootStore.walletSectionInst.runKeypairImportPopup()
}
}
RowLayout {
Layout.fillWidth: true
StatusTabBar {
id: walletTabBar
objectName: "rightSideWalletTabBar"
Layout.fillWidth: true
Layout.topMargin: Theme.padding
StatusTabButton {
objectName: "assetsTabButton"
width: implicitWidth
text: d.assetsString
}
StatusTabButton {
objectName: "collectiblesTabButton"
width: implicitWidth
text: d.collectiblesString
}
StatusTabButton {
objectName: "activityTabButton"
rightPadding: 0
width: implicitWidth
text: d.historyString
}
onCurrentIndexChanged: {
RootStore.setCurrentViewedHoldingType(walletTabBar.currentIndex === 1 ? Constants.TokenType.ERC721 : Constants.TokenType.ERC20)
}
}
// Ownership check running in the background. It lives in the tab
// header, next to the filter button, so that starting/finishing a
// check never shifts or overlays the list below: the row's height
// is set by the tab bar and the filter button, and the tab bar
// simply yields the few pixels it takes.
StatusLoadingIndicator {
objectName: "collectiblesOwnershipCheckIndicator"
Layout.alignment: Qt.AlignVCenter
Layout.rightMargin: Theme.halfPadding
Layout.preferredWidth: 16
Layout.preferredHeight: 16
color: Theme.palette.baseColor1
visible: walletTabBar.currentIndex === RightTabView.TabIndex.Collectibles
&& RootStore.collectiblesStore.areCollectiblesUpdating
StatusToolTip {
visible: hoverHandler.hovered
text: qsTr("Checking collectibles ownership…")
}
HoverHandler {
id: hoverHandler
}
}
StatusFlatButton {
id: filterButton
objectName: "filterButton"
icon.name: "filter"
checkable: true
icon.color: checked ? Theme.palette.primaryColor1 : Theme.palette.baseColor1
Behavior on icon.color { ColorAnimation { duration: 200; easing.type: Easing.InOutQuad } }
highlighted: checked
visible: walletTabBar.currentIndex !== RightTabView.TabIndex.Activity // TODO #16761: Re-enable filter for activity when implemented
}
}
// Per-tab skeleton shown while the tab view incubates; takes the
// loader's layout slot while the loader is hidden. Built from plain
// LoadingComponent shapes — the real loading delegates
// (LoadingTokenDelegate & co) are too expensive to create here.
Loader {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.topMargin: Theme.padding
active: mainViewLoader.status !== Loader.Ready
visible: active
sourceComponent: {
switch (walletTabBar.currentIndex) {
case RightTabView.TabIndex.Collectibles:
return collectiblesSkeleton
case RightTabView.TabIndex.Activity:
return activitySkeleton
default:
return assetsSkeleton
}
}
}
Component {
id: assetsSkeleton
WalletAssetListSkeleton {}
}
// cell metrics mirror CollectiblesView
Component {
id: collectiblesSkeleton
LoadingSkeletonGroup {
id: collectiblesSkeletonRoot
readonly property bool compact: width < 600
readonly property int itemSpacing: compact ? Theme.halfPadding : 0
readonly property int cellWidth: compact ? Math.floor(width / 3) : 176
readonly property int cellHeight: compact ? cellWidth + 49 : 225
Flow {
anchors.fill: parent
spacing: collectiblesSkeletonRoot.itemSpacing
Repeater {
model: 6
ColumnLayout {
width: collectiblesSkeletonRoot.cellWidth - collectiblesSkeletonRoot.itemSpacing
height: collectiblesSkeletonRoot.cellHeight - collectiblesSkeletonRoot.itemSpacing
spacing: Theme.halfPadding
LoadingSkeletonTile {
Layout.fillWidth: true
Layout.fillHeight: true
radius: Theme.radius
}
LoadingSkeletonTile {
implicitWidth: 90
implicitHeight: 14
}
LoadingSkeletonTile {
Layout.bottomMargin: Theme.halfPadding
implicitWidth: 60
implicitHeight: 12
}
}
}
}
}
}
Component {
id: activitySkeleton
LoadingSkeletonGroup {
ColumnLayout {
anchors {
top: parent.top
left: parent.left
right: parent.right
}
spacing: Theme.padding
Repeater {
model: 6
RowLayout {
Layout.fillWidth: true
Layout.preferredHeight: 56
spacing: Theme.padding
LoadingSkeletonTile {
implicitWidth: 32
implicitHeight: 32
radius: width / 2
}
ColumnLayout {
Layout.fillWidth: true
spacing: Theme.halfPadding
LoadingSkeletonTile {
implicitWidth: 140
implicitHeight: 14
}
LoadingSkeletonTile {
implicitWidth: 100
implicitHeight: 12
}
}
LoadingSkeletonTile {
Layout.alignment: Qt.AlignRight
implicitWidth: 70
implicitHeight: 14
}
}
}
}
}
}
Loader {
id: mainViewLoader
objectName: "walletMainViewLoader"
Layout.fillWidth: true
Layout.fillHeight: true
Layout.topMargin: Theme.padding
asynchronous: true
visible: status === Loader.Ready
sourceComponent: d.walletViewsMap[walletTabBar.currentIndex]
Component {
id: assetsView
AssetsView {
id: walletAssetsView
formatBalance: (balance, key) => {
return LocaleUtils.currencyAmountToLocaleString(
RootStore.currencyStore.getCurrencyAmount(balance, key))
}
chainsError: (chains) => {
if (!root.networkConnectionStore)
return ""
return root.networkConnectionStore.getBlockchainNetworkDownText(chains)
}
onSortRequested: (roleName, order) => RootStore.walletAssetsStore.sortAssets(roleName, order)
function refreshSortSettings() {
settings.category = settingsCategoryName
walletSettings.sync()
settings.sync()
if (walletSettings.assetsViewCustomOrderApplyTimestamp > settings.sortOrderUpdateTimestamp && customOrderAvailable) {
sortByValue(SortOrderComboBox.TokenOrderCustom)
setSortOrder(Qt.AscendingOrder) // same as SortOrderComboBox.onActivated for Custom order
} else {
sortByValue(settings.currentSortValue)
setSortOrder(settings.currentSortOrder)
}
}
function saveSortSettings() {
settings.currentSortValue = getSortValue()
settings.currentSortOrder = getSortOrder()
settings.sortOrderUpdateTimestamp = new Date().getTime()
settings.sync()
}
readonly property string settingsCategoryName: {
const addressFilters = RootStore.addressFilters
return "AssetsViewSortSettings-" + (addressFilters.indexOf(':') > -1 ? "all" : addressFilters)
}
onSettingsCategoryNameChanged: {
saveSortSettings()
refreshSortSettings()
}
Component.onCompleted: refreshSortSettings()
Component.onDestruction: saveSortSettings()
readonly property var walletSettings: Settings { /* https://bugreports.qt.io/browse/QTBUG-135039 */
id: walletSettings
category: "walletSettings-" + userProfile.pubKey
property var assetsViewCustomOrderApplyTimestamp
onAssetsViewCustomOrderApplyTimestampChanged: walletAssetsView.refreshSortSettings()
}
readonly property var settings: Settings { /* https://bugreports.qt.io/browse/QTBUG-135039 */
id: settings
property int currentSortValue: SortOrderComboBox.TokenOrderDateAdded
property var sortOrderUpdateTimestamp
property int currentSortOrder: Qt.DescendingOrder
}
loading: RootStore.overview.balanceLoading
sorterVisible: filterButton.checked
customOrderAvailable: RootStore.walletAssetsStore.assetsController.hasSettings
model: RootStore.walletAssetsStore.assetsModel
bannerComponent: buyReceiveBannerComponent
marketDataError: !!root.networkConnectionStore
? root.networkConnectionStore.getMarketNetworkDownText()
: ""
balanceError: {
if (!root.networkConnectionStore)
return ""
return (root.networkConnectionStore.noBlockchainConnectionAndNoCache
&& !root.networkConnectionStore.noMarketConnectionAndNoCache)
? root.networkConnectionStore.noBlockchainConnectionAndNoCacheText
: ""
}
formatFiat: balance => RootStore.currencyStore.formatCurrencyAmount(
balance, RootStore.currencyStore.currentCurrency)
sendEnabled: root.networkConnectionStore.walletReadyForTransactionsEnabled &&
!RootStore.overview.isWatchOnlyAccount && RootStore.overview.canSend
communitySendEnabled: RootStore.tokensStore.showCommunityAssetsInSend
swapEnabled: !RootStore.overview.isWatchOnlyAccount
swapVisible: root.swapEnabled
onSendRequested: (key) => {
root.sendTokenRequested(RootStore.overview.mixedcaseAddress.toLowerCase(),
key, Constants.TokenType.ERC20)
}
onSwapRequested: (key) => root.launchSwapModal(key)
onReceiveRequested: root.launchShareAddressModal()
onCommunityClicked: Global.switchToCommunity(communityKey)
onHideRequested: (key) => {
const token = SQUtils.ModelUtils.getByKey(RootStore.walletAssetsStore.groupedAccountAssetsModel, "key", key)
Global.openConfirmHideAssetPopup(token.symbol, token.name, token.logoUri || Constants.tokenIcon(token.symbol, false), !!token.communityId)
}
onHideCommunityAssetsRequested:
(communityKey) => {
const community = SQUtils.ModelUtils.getByKey(RootStore.walletAssetsStore.groupedAccountAssetsModel, "communityId", communityKey)
confirmHideCommunityAssetsPopup.createObject(root, {
name: community.communityName,
icon: community.communityImage,
communityId: communityKey }
).open()
}
onManageTokensRequested: Global.changeAppSectionBySectionType(
Constants.appSection.profile,
Constants.settingsSubsection.wallet,
Constants.walletSettingsSubsection.manageAssets)
onAssetClicked: (key) => {
const tokenGroup = SQUtils.ModelUtils.getByKey(RootStore.walletAssetsStore.groupedAccountAssetsModel, "key", key)
const listAsset = SQUtils.ModelUtils.getByKey(RootStore.walletAssetsStore.assetsModel, "key", key)
d.assetDetailTokenGroup = listAsset ? Object.assign({}, tokenGroup, {
balance: listAsset.balance,
balanceLoading: listAsset.balanceLoading,
marketPrice: listAsset.marketPrice,
balanceText: LocaleUtils.currencyAmountToLocaleString(
RootStore.currencyStore.getCurrencyAmount(listAsset.balance, key)),
}) : tokenGroup
RootStore.setCurrentViewedHolding(tokenGroup.key, Constants.TokenType.ERC20, tokenGroup.communityId ?? "")
stack.currentIndex = 2
}
}
}
Component {
id: collectiblesView
CollectiblesView {
id: collView
function refreshSortSettings() {
settings.category = settingsCategoryName
walletSettings.sync()
settings.sync()
if (walletSettings.collectiblesViewCustomOrderApplyTimestamp > settings.sortOrderUpdateTimestamp && customOrderAvailable) {
sortByValue(SortOrderComboBox.TokenOrderCustom)
setSortOrder(Qt.AscendingOrder) // same as SortOrderComboBox.onActivated for Custom order
} else {
sortByValue(settings.currentSortValue)
setSortOrder(settings.currentSortOrder)
}
}
function saveSortSettings() {
settings.currentSortValue = getSortValue()
settings.currentSortOrder = getSortOrder()
settings.sortOrderUpdateTimestamp = new Date().getTime()
settings.sync()
}
readonly property string settingsCategoryName: "CollectiblesViewSortSettings-" + (addressFilters.indexOf(':') > -1 ? "all" : addressFilters)
onSettingsCategoryNameChanged: {
saveSortSettings()
refreshSortSettings()
}
Component.onCompleted: refreshSortSettings()
Component.onDestruction: saveSortSettings()
Connections {
target: walletSettings
function onCollectiblesViewCustomOrderApplyTimestampChanged() {
collView.refreshSortSettings()
}
}
readonly property var settings: Settings { /* https://bugreports.qt.io/browse/QTBUG-135039 */
id: settings
property int currentSortValue: SortOrderComboBox.TokenOrderDateAdded
property real sortOrderUpdateTimestamp: 0
property alias selectedFilterGroupIds: collView.selectedFilterGroupIds
property int currentSortOrder: Qt.DescendingOrder
}
ownedAccountsModel: RootStore.nonWatchAccounts
controller: RootStore.collectiblesStore.collectiblesController
activeNetworks: root.networksStore.activeNetworks
networkFilters: root.networksStore.networkFilters
addressFilters: RootStore.addressFilters
unsupportedChainIds: root.networkConnectionStore.unsupportedCollectibleChains
sendEnabled: root.networkConnectionStore.walletReadyForTransactionsEnabled && !RootStore.overview.isWatchOnlyAccount && RootStore.overview.canSend
filterVisible: filterButton.checked
customOrderAvailable: controller.hasSettings
bannerComponent: buyReceiveBannerComponent
onCollectibleClicked: function (chainId, contractAddress, tokenId, uid, tokenType, communityId) {
RootStore.collectiblesStore.getDetailedCollectible(chainId, contractAddress, tokenId)
RootStore.setCurrentViewedHolding(uid, tokenType, communityId)
d.detailedCollectibleActivityController.resetFilter()
d.detailedCollectibleActivityController.setFilterAddressesJson(JSON.stringify(RootStore.addressFilters.split(":")))
d.detailedCollectibleActivityController.setFilterChainsJson(JSON.stringify([chainId]), false)
d.detailedCollectibleActivityController.setFilterCollectibles(JSON.stringify([uid]))
d.detailedCollectibleActivityController.updateFilter()
stack.currentIndex = 1
}
onSendRequested: function (collectionUid, tokenType, fromAddress) {
const collectible = SQUtils.ModelUtils.getByKey(controller.sourceModel, "collectionUid", collectionUid)
if (!!collectible &&
(collectible.soulbound ||
collectible.communityPrivilegesLevel === Constants.TokenPrivilegesLevel.Owner)) {
return
}
root.sendTokenRequested(fromAddress, collectionUid, tokenType)
}
onReceiveRequested: (symbol) => root.launchShareAddressModal()
onSwitchToCommunityRequested: (communityId) => Global.switchToCommunity(communityId)
onManageTokensRequested: Global.changeAppSectionBySectionType(Constants.appSection.profile, Constants.settingsSubsection.wallet,
Constants.walletSettingsSubsection.manageCollectibles)
isError: RootStore.collectiblesStore.areCollectiblesError
}
}
Component {
id: historyView
HistoryView {
overview: RootStore.overview
activityStore: RootStore
communitiesStore: root.communitiesStore
currencyStore: root.sharedRootStore.currencyStore
networksStore: root.networksStore
showAllAccounts: RootStore.showAllAccounts
filterVisible: false // TODO #16761: Re-enable filter for activity when implemented
bannerComponent: buyReceiveBannerComponent
}
}
}
}
// Both detail views are built on navigation into their stack index and
// unloaded on the way out, so a section load never pays for a view the
// user has not opened. The reset that used to hang off `visible` now
// hangs off `active`: an unloaded view never sees a visibility change.
Loader {
id: collectibleDetailLoader
objectName: "collectibleDetailLoader"
asynchronous: true
active: stack.currentIndex === 1
onActiveChanged: {
if (!active) {
RootStore.resetCurrentViewedHolding(Constants.TokenType.ERC721)
RootStore.collectiblesStore.resetDetailedCollectible()
}
}
sourceComponent: CollectibleDetailView {
objectName: "collectibleDetailView"
collectible: RootStore.collectiblesStore.detailedCollectible
isCollectibleLoading: RootStore.collectiblesStore.isDetailedCollectibleLoading
activityModel: d.detailedCollectibleActivityController.model
addressFilters: RootStore.addressFilters
rootStore: root.sharedRootStore
walletRootStore: RootStore
communitiesStore: root.communitiesStore
networksStore: root.networksStore
}
}
Loader {
id: assetDetailLoader
objectName: "assetDetailLoader"
asynchronous: true
active: stack.currentIndex === 2
onActiveChanged: {
if (!active)
RootStore.resetCurrentViewedHolding(Constants.TokenType.ERC20)
}
sourceComponent: AssetsDetailView {
objectName: "assetDetailView"
tokenGroup: d.assetDetailTokenGroup ?? ({})
tokensStore: RootStore.tokensStore
allNetworksModel: root.networksStore.activeNetworks
address: RootStore.overview.mixedcaseAddress
currencyStore: RootStore.currencyStore
networkFilters: root.networksStore.networkFilters
networkConnectionStore: root.networkConnectionStore
}
}
}
}