mirror of
https://github.com/status-im/status-app.git
synced 2026-08-27 07:01:14 +00:00
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`.
752 lines
34 KiB
QML
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
|
|
}
|
|
}
|
|
}
|
|
}
|