From 072537f61a09ec772265e4cbda1903d4784491af Mon Sep 17 00:00:00 2001 From: Khushboo Mehta Date: Wed, 15 Mar 2023 10:17:25 +0100 Subject: [PATCH] feat(@desktop/wallet): Implement connection error screens fixes #9835 --- src/app/modules/main/module.nim | 3 +- .../main/network_connection/module.nim | 2 +- .../network_connection_item.nim | 101 ++++++++++++++++++ .../modules/main/network_connection/view.nim | 48 ++++++++- .../collectibles/controller.nim | 24 ++++- .../collectibles/io_interface.nim | 3 + .../models/collectibles_model.nim | 5 + .../wallet_section/collectibles/module.nim | 12 ++- .../main/wallet_section/collectibles/view.nim | 3 + .../modules/main/wallet_section/module.nim | 10 +- .../service/collectible/async_tasks.nim | 2 +- .../service/collectible/service.nim | 6 +- .../service/network_connection/service.nim | 11 +- .../service/wallet_account/service.nim | 12 +++ .../ui-test/src/screens/StatusWalletScreen.py | 2 + test/ui-test/testSuites/suite_wallet/dev/.env | 2 +- .../suite_wallet/tst_transaction/test.feature | 2 +- .../src/StatusQ/Components/StatusListItem.qml | 15 +++ .../Wallet/controls/FooterTooltipButton.qml | 29 +++++ .../AppLayouts/Wallet/panels/WalletFooter.qml | 25 +++-- .../AppLayouts/Wallet/panels/WalletHeader.qml | 2 + .../Wallet/views/AssetsDetailView.qml | 52 +++++---- .../AppLayouts/Wallet/views/LeftTabView.qml | 19 ++++ .../AppLayouts/Wallet/views/RightTabView.qml | 6 ++ ui/app/AppLayouts/stores/RootStore.qml | 25 ----- ui/app/mainui/AppMain.qml | 2 +- .../shared/controls/AssetsDetailsHeader.qml | 29 +++-- .../shared/controls/InformationTile.qml | 6 +- ui/imports/shared/controls/TokenDelegate.qml | 28 +++++ .../shared/panels/ConnectionWarnings.qml | 7 +- ui/imports/shared/popups/SendModal.qml | 8 ++ .../shared/stores/NetworkConnectionStore.qml | 95 ++++++++++++++++ ui/imports/shared/stores/qmldir | 1 + ui/imports/shared/views/AssetsView.qml | 9 +- 34 files changed, 517 insertions(+), 89 deletions(-) create mode 100644 src/app/modules/main/network_connection/network_connection_item.nim create mode 100644 ui/app/AppLayouts/Wallet/controls/FooterTooltipButton.qml create mode 100644 ui/imports/shared/stores/NetworkConnectionStore.qml diff --git a/src/app/modules/main/module.nim b/src/app/modules/main/module.nim index 68f9cb5065..d4bfc8eddc 100644 --- a/src/app/modules/main/module.nim +++ b/src/app/modules/main/module.nim @@ -190,7 +190,8 @@ proc newModule*[T]( result.walletSectionModule = wallet_section_module.newModule( result, events, tokenService, currencyService, transactionService, collectible_service, walletAccountService, - settingsService, savedAddressService, networkService, accountsService, keycardService + settingsService, savedAddressService, networkService, accountsService, + keycardService, nodeService, networkConnectionService ) result.browserSectionModule = browser_section_module.newModule( result, events, bookmarkService, settingsService, networkService, diff --git a/src/app/modules/main/network_connection/module.nim b/src/app/modules/main/network_connection/module.nim index e18c521a64..b8a0fffc31 100644 --- a/src/app/modules/main/network_connection/module.nim +++ b/src/app/modules/main/network_connection/module.nim @@ -50,7 +50,7 @@ method viewDidLoad*(self: Module) = self.checkIfModuleDidLoad() method networkConnectionStatusUpdate*(self: Module, website: string, completelyDown: bool, connectionState: int, chainIds: string, lastCheckedAt: int, timeToAutoRetryInSecs: int, withCache: bool) = - self.view.networkConnectionStatusUpdate(website, completelyDown, connectionState, chainIds, lastCheckedAt, timeToAutoRetryInSecs, withCache) + self.view.updateNetworkConnectionStatus(website, completelyDown, connectionState, chainIds, lastCheckedAt, timeToAutoRetryInSecs, withCache) method refreshBlockchainValues*(self: Module) = self.controller.refreshBlockchainValues() diff --git a/src/app/modules/main/network_connection/network_connection_item.nim b/src/app/modules/main/network_connection/network_connection_item.nim new file mode 100644 index 0000000000..c704a17f16 --- /dev/null +++ b/src/app/modules/main/network_connection/network_connection_item.nim @@ -0,0 +1,101 @@ +import NimQml, strformat + +QtObject: + type NetworkConnectionItem* = ref object of QObject + completelyDown: bool + connectionState: int + chainIds: string + lastCheckedAt: int + timeToAutoRetryInSecs: int + withCache: bool + + proc delete*(self: NetworkConnectionItem) = + self.QObject.delete + + proc newNetworkConnectionItem*(completelyDown = false, connectionState = 0, chainIds = "", lastCheckedAt = 0, timeToAutoRetryInSecs = 0, withCache = false,): NetworkConnectionItem = + new(result, delete) + result.QObject.setup + result.completelyDown = completelyDown + result.connectionState = connectionState + result.chainIds = chainIds + result.lastCheckedAt = lastCheckedAt + result.timeToAutoRetryInSecs = timeToAutoRetryInSecs + result.withCache = withCache + + proc `$`*(self: NetworkConnectionItem): string = + result = fmt"""NetworkConnectionItem[ + completelyDown: {self.completelyDown}, + connectionState: {self.connectionState}, + chainIds: {self.chainIds}, + lastCheckedAt: {self.lastCheckedAt}, + timeToAutoRetryInSecs: {self.timeToAutoRetryInSecs}, + withCache: {self.withCache} + ]""" + + proc completelyDownChanged*(self: NetworkConnectionItem) {.signal.} + proc getCompletelyDown*(self: NetworkConnectionItem): bool {.slot.} = + return self.completelyDown + QtProperty[bool] completelyDown: + read = getCompletelyDown + notify = completelyDownChanged + + proc connectionStateChanged*(self: NetworkConnectionItem) {.signal.} + proc getConnectionState*(self: NetworkConnectionItem): int {.slot.} = + return self.connectionState + QtProperty[int] connectionState: + read = getConnectionState + notify = connectionStateChanged + + proc chainIdsChanged*(self: NetworkConnectionItem) {.signal.} + proc getChainIds*(self: NetworkConnectionItem): string {.slot.} = + return self.chainIds + QtProperty[string] chainIds: + read = getChainIds + notify = chainIdsChanged + + proc lastCheckedAtChanged*(self: NetworkConnectionItem) {.signal.} + proc getLastCheckedAt*(self: NetworkConnectionItem): int {.slot.} = + return self.lastCheckedAt + QtProperty[int] lastCheckedAt: + read = getLastCheckedAt + notify = lastCheckedAtChanged + + proc timeToAutoRetryInSecsChanged*(self: NetworkConnectionItem) {.signal.} + proc getTimeToAutoRetryInSecs*(self: NetworkConnectionItem): int {.slot.} = + return self.timeToAutoRetryInSecs + QtProperty[int] timeToAutoRetryInSecs: + read = getTimeToAutoRetryInSecs + notify = timeToAutoRetryInSecsChanged + + proc withCacheChanged*(self: NetworkConnectionItem) {.signal.} + proc getWithCache*(self: NetworkConnectionItem): bool {.slot.} = + return self.withCache + QtProperty[bool] withCache: + read = getWithCache + notify = withCacheChanged + + proc updateValues*(self: NetworkConnectionItem, completelyDown: bool, connectionState: int, + chainIds: string, lastCheckedAt: int, timeToAutoRetryInSecs: int, withCache: bool) = + if self.completelyDown != completelyDown : + self.completelyDown = completelyDown + self.completelyDownChanged() + + if self.connectionState != connectionState : + self.connectionState = connectionState + self.connectionStateChanged() + + if self.chainIds != chainIds : + self.chainIds = chainIds + self.chainIdsChanged() + + if self.lastCheckedAt != lastCheckedAt : + self.lastCheckedAt = lastCheckedAt + self.lastCheckedAtChanged() + + if self.timeToAutoRetryInSecs != timeToAutoRetryInSecs : + self.timeToAutoRetryInSecs = timeToAutoRetryInSecs + self.timeToAutoRetryInSecsChanged() + + if self.withCache != withCache : + self.withCache = withCache + self.withCacheChanged() diff --git a/src/app/modules/main/network_connection/view.nim b/src/app/modules/main/network_connection/view.nim index 5307a2dfa8..8533653ba1 100644 --- a/src/app/modules/main/network_connection/view.nim +++ b/src/app/modules/main/network_connection/view.nim @@ -1,27 +1,57 @@ import NimQml import ./io_interface +import ./network_connection_item +import ../../../../app_service/service/network_connection/service as network_connection_service QtObject: type View* = ref object of QObject delegate: io_interface.AccessInterface + blockchainNetworkConnection: NetworkConnectionItem + collectiblesNetworkConnection: NetworkConnectionItem + marketValuesNetworkConnection: NetworkConnectionItem proc setup(self: View) = self.QObject.setup proc delete*(self: View) = self.QObject.delete + self.blockchainNetworkConnection.delete + self.collectiblesNetworkConnection.delete + self.marketValuesNetworkConnection.delete proc newView*(delegate: io_interface.AccessInterface): View = new(result, delete) result.delegate = delegate + result.blockchainNetworkConnection = newNetworkConnectionItem() + result.collectiblesNetworkConnection = newNetworkConnectionItem() + result.marketValuesNetworkConnection = newNetworkConnectionItem() result.setup() proc load*(self: View) = self.delegate.viewDidLoad() - proc networkConnectionStatusUpdate*(self: View, website: string, completelyDown: bool, connectionState: int, chainIds: string, lastCheckedAt: int, timeToAutoRetryInSecs: int, withCache: bool) {.signal.} + proc blockchainNetworkConnectionChanged*(self:View) {.signal.} + proc getBlockchainNetworkConnection(self: View): QVariant {.slot.} = + return newQVariant(self.blockchainNetworkConnection) + QtProperty[QVariant] blockchainNetworkConnection: + read = getBlockchainNetworkConnection + notify = blockchainNetworkConnectionChanged + + proc collectiblesNetworkConnectionChanged*(self:View) {.signal.} + proc getCollectiblesNetworkConnection(self: View): QVariant {.slot.} = + return newQVariant(self.collectiblesNetworkConnection) + QtProperty[QVariant] collectiblesNetworkConnection: + read = getCollectiblesNetworkConnection + notify = collectiblesNetworkConnectionChanged + + proc marketValuesNetworkConnectionChanged*(self:View) {.signal.} + proc getMarketValuesNetworkConnection(self: View): QVariant {.slot.} = + return newQVariant(self.marketValuesNetworkConnection) + QtProperty[QVariant] marketValuesNetworkConnection: + read = getMarketValuesNetworkConnection + notify = marketValuesNetworkConnectionChanged proc refreshBlockchainValues*(self: View) {.slot.} = self.delegate.refreshBlockchainValues() @@ -32,3 +62,19 @@ QtObject: proc refreshCollectiblesValues*(self: View) {.slot.} = self.delegate.refreshCollectiblesValues() + proc networkConnectionStatusUpdate*(self: View, website: string, completelyDown: bool, connectionState: int, chainIds: string, lastCheckedAt: int, timeToAutoRetryInSecs: int, withCache: bool) {.signal.} + + proc updateNetworkConnectionStatus*(self: View, website: string, completelyDown: bool, connectionState: int, chainIds: string, lastCheckedAt: int, timeToAutoRetryInSecs: int, withCache: bool) = + case website: + of BLOCKCHAINS: + self.blockchainNetworkConnection.updateValues(completelyDown, connectionState, chainIds, lastCheckedAt, timeToAutoRetryInSecs, withCache) + self.blockchainNetworkConnectionChanged() + of COLLECTIBLES: + self.collectiblesNetworkConnection.updateValues(completelyDown, connectionState, chainIds, lastCheckedAt, timeToAutoRetryInSecs, withCache) + self.collectiblesNetworkConnectionChanged() + of MARKET: + self.marketValuesNetworkConnection.updateValues(completelyDown, connectionState, chainIds, lastCheckedAt, timeToAutoRetryInSecs, withCache) + self.marketValuesNetworkConnectionChanged() + self.networkConnectionStatusUpdate(website, completelyDown, connectionState, chainIds, lastCheckedAt, timeToAutoRetryInSecs, withCache) + + diff --git a/src/app/modules/main/wallet_section/collectibles/controller.nim b/src/app/modules/main/wallet_section/collectibles/controller.nim index b7baa6feb7..33153c731c 100644 --- a/src/app/modules/main/wallet_section/collectibles/controller.nim +++ b/src/app/modules/main/wallet_section/collectibles/controller.nim @@ -4,6 +4,7 @@ import ../../../../../app_service/service/collectible/service as collectible_ser import ../../../../../app_service/service/wallet_account/service as wallet_account_service import ../../../../../app_service/service/network/service as network_service import ../../../../../app_service/service/network_connection/service as network_connection_service +import ../../../../../app_service/service/node/service as node_service import ../../../../core/eventemitter type @@ -13,13 +14,20 @@ type collectibleService: collectible_service.Service walletAccountService: wallet_account_service.Service networkService: network_service.Service + nodeService: node_service.Service + networkConnectionService: network_connection_service.Service + + # Forward declaration +proc resetOwnedCollectibles*(self: Controller, chainId: int, address: string) proc newController*( delegate: io_interface.AccessInterface, events: EventEmitter, collectibleService: collectible_service.Service, walletAccountService: wallet_account_service.Service, - networkService: network_service.Service + networkService: network_service.Service, + nodeService: node_service.Service, + networkConnectionService: network_connection_service.Service ): Controller = result = Controller() result.delegate = delegate @@ -27,6 +35,8 @@ proc newController*( result.collectibleService = collectibleService result.walletAccountService = walletAccountService result.networkService = networkService + result.nodeService = nodeService + result.networkConnectionService = networkConnectionService proc delete*(self: Controller) = discard @@ -37,6 +47,8 @@ proc refreshCollectibles(self: Controller, chainId: int, address: string) = self.delegate.setCollectibles(chainId, address, data) else: self.delegate.appendCollectibles(chainId, address, data) + if not self.nodeService.isConnected() or not self.networkConnectionService.checkIfConnected(COLLECTIBLES): + self.delegate.noConnectionToOpenSea() proc init*(self: Controller) = self.events.on(SIGNAL_OWNED_COLLECTIBLES_RESET) do(e:Args): @@ -55,7 +67,15 @@ proc init*(self: Controller) = let args = RetryCollectibleArgs(e) let chainId = self.networkService.getNetworkForCollectibles().chainId for address in args.addresses: - self.refreshCollectibles(chainId, address) + self.resetOwnedCollectibles(chainId, address) + + self.events.on(SIGNAL_NETWORK_DISCONNECTED) do(e: Args): + self.delegate.noConnectionToOpenSea() + + self.events.on(SIGNAL_CONNECTION_UPDATE) do(e:Args): + let args = NetworkConnectionsArgs(e) + if args.website == COLLECTIBLES and args.completelyDown: + self.delegate.noConnectionToOpenSea() proc getWalletAccount*(self: Controller, accountIndex: int): wallet_account_service.WalletAccountDto = return self.walletAccountService.getWalletAccount(accountIndex) diff --git a/src/app/modules/main/wallet_section/collectibles/io_interface.nim b/src/app/modules/main/wallet_section/collectibles/io_interface.nim index 83b839e36f..e9f0d9957c 100644 --- a/src/app/modules/main/wallet_section/collectibles/io_interface.nim +++ b/src/app/modules/main/wallet_section/collectibles/io_interface.nim @@ -37,3 +37,6 @@ method collectiblesModuleDidLoad*(self: AccessInterface) {.base.} = method currentCollectibleModuleDidLoad*(self: AccessInterface) {.base.} = raise newException(ValueError, "No implementation available") + +method noConnectionToOpenSea*(self: AccessInterface) {.base.} = + raise newException(ValueError, "No implementation available") diff --git a/src/app/modules/main/wallet_section/collectibles/models/collectibles_model.nim b/src/app/modules/main/wallet_section/collectibles/models/collectibles_model.nim index 20cf4cc75d..48d7d2b0d4 100644 --- a/src/app/modules/main/wallet_section/collectibles/models/collectibles_model.nim +++ b/src/app/modules/main/wallet_section/collectibles/models/collectibles_model.nim @@ -203,3 +203,8 @@ QtObject: self.items = concat(self.items, items) self.endInsertRows() self.countChanged() + + # in case loading is still going on and no items are present, show loading items when there is no connection to opensea possible + proc noConnectionToOpenSea*(self: Model) = + if self.items.len == 0: + self.setIsFetching(true) diff --git a/src/app/modules/main/wallet_section/collectibles/module.nim b/src/app/modules/main/wallet_section/collectibles/module.nim index a1271e3fc2..05b5e8a06b 100644 --- a/src/app/modules/main/wallet_section/collectibles/module.nim +++ b/src/app/modules/main/wallet_section/collectibles/module.nim @@ -8,6 +8,8 @@ import ../io_interface as delegate_interface import ../../../../../app_service/service/collectible/service as collectible_service import ../../../../../app_service/service/wallet_account/service as wallet_account_service import ../../../../../app_service/service/network/service as network_service +import ../../../../../app_service/service/node/service as node_service +import ../../../../../app_service/service/network_connection/service as network_connection_service import ./current_collectible/module as current_collectible_module @@ -34,12 +36,14 @@ proc newModule*( events: EventEmitter, collectibleService: collectible_service.Service, walletAccountService: wallet_account_service.Service, - networkService: network_service.Service + networkService: network_service.Service, + nodeService: node_service.Service, + networkConnectionService: network_connection_service.Service ): Module = result = Module() result.delegate = delegate result.view = newView(result) - result.controller = newController(result, events, collectibleService, walletAccountService, networkService) + result.controller = newController(result, events, collectibleService, walletAccountService, networkService, nodeService, networkConnectionService) result.moduleLoaded = false result.currentCollectibleModule = currentCollectibleModule.newModule(result, collectibleService) @@ -109,7 +113,6 @@ method setCollectibles*(self: Module, chainId: int, address: string, data: Colle self.view.setCollectibles(newCollectibles) self.view.setAllLoaded(data.allLoaded) - method appendCollectibles*(self: Module, chainId: int, address: string, data: CollectiblesData) = if self.chainId == chainId and self.address == address: self.view.setIsFetching(data.isFetching) @@ -122,3 +125,6 @@ method appendCollectibles*(self: Module, chainId: int, address: string, data: Co self.view.appendCollectibles(newCollectibles) self.view.setAllLoaded(data.allLoaded) + +method noConnectionToOpenSea*(self: Module) = + self.view.noConnectionToOpenSea() diff --git a/src/app/modules/main/wallet_section/collectibles/view.nim b/src/app/modules/main/wallet_section/collectibles/view.nim index d40be2de42..ddcb8aebff 100644 --- a/src/app/modules/main/wallet_section/collectibles/view.nim +++ b/src/app/modules/main/wallet_section/collectibles/view.nim @@ -45,3 +45,6 @@ QtObject: proc appendCollectibles*(self: View, collectibles: seq[Item]) = self.model.appendItems(collectibles) + + proc noConnectionToOpenSea*(self: View) = + self.model.noConnectionToOpenSea() diff --git a/src/app/modules/main/wallet_section/module.nim b/src/app/modules/main/wallet_section/module.nim index 6ee4dcef0d..c15989ec3f 100644 --- a/src/app/modules/main/wallet_section/module.nim +++ b/src/app/modules/main/wallet_section/module.nim @@ -24,6 +24,8 @@ import ../../../../app_service/service/settings/service as settings_service import ../../../../app_service/service/saved_address/service as saved_address_service import ../../../../app_service/service/network/service as network_service import ../../../../app_service/service/accounts/service as accounts_service +import ../../../../app_service/service/node/service as node_service +import ../../../../app_service/service/network_connection/service as network_connection_service import io_interface export io_interface @@ -57,7 +59,9 @@ proc newModule*( savedAddressService: saved_address_service.Service, networkService: network_service.Service, accountsService: accounts_service.Service, - keycardService: keycard_service.Service + keycardService: keycard_service.Service, + nodeService: node_service.Service, + networkConnectionService: network_connection_service.Service ): Module = result = Module() result.delegate = delegate @@ -68,7 +72,7 @@ proc newModule*( result.accountsModule = accounts_module.newModule(result, events, keycardService, walletAccountService, accountsService, networkService, tokenService, currencyService) result.allTokensModule = all_tokens_module.newModule(result, events, tokenService, walletAccountService) - result.collectiblesModule = collectibles_module.newModule(result, events, collectibleService, walletAccountService, networkService) + result.collectiblesModule = collectibles_module.newModule(result, events, collectibleService, walletAccountService, networkService, nodeService, networkConnectionService) result.currentAccountModule = current_account_module.newModule(result, events, walletAccountService, networkService, tokenService, currencyService) result.transactionsModule = transactions_module.newModule(result, events, transactionService, walletAccountService, networkService, currencyService) result.savedAddressesModule = saved_addresses_module.newModule(result, events, savedAddressService) @@ -121,6 +125,8 @@ method load*(self: Module) = self.events.on(SIGNAL_WALLET_ACCOUNT_TOKENS_REBUILT) do(e:Args): self.setTotalCurrencyBalance() self.view.setTokensLoading(false) + self.events.on(SIGNAL_WALLET_ACCOUNT_TOKENS_BEING_FETCHED) do(e:Args): + self.view.setTokensLoading(true) self.events.on(SIGNAL_CURRENCY_FORMATS_UPDATED) do(e:Args): self.setTotalCurrencyBalance() diff --git a/src/app_service/service/collectible/async_tasks.nim b/src/app_service/service/collectible/async_tasks.nim index e934af4f02..862873a6fd 100644 --- a/src/app_service/service/collectible/async_tasks.nim +++ b/src/app_service/service/collectible/async_tasks.nim @@ -11,7 +11,7 @@ const fetchOwnedCollectiblesTaskArg: Task = proc(argEncoded: string) {.gcsafe, n "chainId": arg.chainId, "address": arg.address, "cursor": arg.cursor, - "collectibles": "" + "collectibles": {"assets":nil,"next":"","previous":""} } try: let response = collectibles.getOpenseaAssetsByOwnerWithCursor(arg.chainId, arg.address, arg.cursor, arg.limit) diff --git a/src/app_service/service/collectible/service.nim b/src/app_service/service/collectible/service.nim index 162fe898b3..c13b3d1628 100644 --- a/src/app_service/service/collectible/service.nim +++ b/src/app_service/service/collectible/service.nim @@ -157,9 +157,9 @@ QtObject: # needs to be re-written once cache for colletibles works proc areCollectionsLoaded*(self: Service): bool = - for chainId, adressesData in self.ownershipData: - for address, collectionsData in adressesData: - if collectionsData.allLoaded: + for chainId, adressesData in self.accountsOwnershipData: + for address, ownershipData in adressesData: + if ownershipData.data.anyLoaded: return true return false diff --git a/src/app_service/service/network_connection/service.nim b/src/app_service/service/network_connection/service.nim index acfa194bd7..9d7dfbae80 100644 --- a/src/app_service/service/network_connection/service.nim +++ b/src/app_service/service/network_connection/service.nim @@ -150,7 +150,7 @@ QtObject: of BLOCKCHAINS: return self.walletService.hasCache() of MARKET: - return self.walletService.hasCache() + return self.walletService.hasMarketCache() of COLLECTIBLES: return self.collectibleService.areCollectionsLoaded() @@ -246,9 +246,6 @@ QtObject: self.events.emit(SIGNAL_CONNECTION_UPDATE, self.convertConnectionStatusToNetworkConnectionsArgs(website, self.connectionStatus[website])) proc checkConnected(self: Service) = - if(not singletonInstance.localAccountSensitiveSettings.getIsWalletEnabled()): - return - try: if self.nodeService.isConnected(): let response = backend.checkConnected() @@ -263,3 +260,9 @@ QtObject: proc networkConnected*(self: Service) = self.walletService.reloadAccountTokens() self.events.emit(SIGNAL_REFRESH_COLLECTIBLES, RetryCollectibleArgs(addresses: self.walletService.getAddresses())) + + proc checkIfConnected*(self: Service, website: string): bool = + if self.connectionStatus.hasKey(website) and self.connectionStatus[website].completelyDown: + return false + return true + diff --git a/src/app_service/service/wallet_account/service.nim b/src/app_service/service/wallet_account/service.nim index 5ab07a9019..930545ce22 100644 --- a/src/app_service/service/wallet_account/service.nim +++ b/src/app_service/service/wallet_account/service.nim @@ -33,6 +33,7 @@ const SIGNAL_WALLET_ACCOUNT_UPDATED* = "walletAccount/walletAccountUpdated" const SIGNAL_WALLET_ACCOUNT_NETWORK_ENABLED_UPDATED* = "walletAccount/networkEnabledUpdated" const SIGNAL_WALLET_ACCOUNT_DERIVED_ADDRESS_READY* = "walletAccount/derivedAddressesReady" const SIGNAL_WALLET_ACCOUNT_TOKENS_REBUILT* = "walletAccount/tokensRebuilt" +const SIGNAL_WALLET_ACCOUNT_TOKENS_BEING_FETCHED* = "walletAccount/tokenFetching" const SIGNAL_WALLET_ACCOUNT_DERIVED_ADDRESS_DETAILS_FETCHED* = "walletAccount/derivedAddressDetailsFetched" const SIGNAL_KEYCARDS_SYNCHRONIZED* = "keycardsSynchronized" @@ -579,6 +580,7 @@ QtObject: accounts: accounts, storeResult: store ) + self.events.emit(SIGNAL_WALLET_ACCOUNT_TOKENS_BEING_FETCHED, Args()) self.threadpool.start(arg) proc getCurrentCurrencyIfEmpty(self: Service, currency = ""): string = @@ -851,3 +853,13 @@ QtObject: if self.walletAccounts[address].tokens.len > 0: return true return false + + proc hasMarketCache*(self: Service): bool = + withLock self.walletAccountsLock: + for address, accountDto in self.walletAccounts: + for token in self.walletAccounts[address].tokens: + for currency, marketValues in token.marketValuesPerCurrency: + if marketValues.highDay > 0: + return true + return false + diff --git a/test/ui-test/src/screens/StatusWalletScreen.py b/test/ui-test/src/screens/StatusWalletScreen.py index a81108e0c8..4f9ed7c4be 100644 --- a/test/ui-test/src/screens/StatusWalletScreen.py +++ b/test/ui-test/src/screens/StatusWalletScreen.py @@ -301,6 +301,8 @@ class StatusWalletScreen: for index in range(list.count): tokenListItem = list.itemAtIndex(index) + if tokenListItem != None and tokenListItem.objectName == "AssetView_LoadingTokenDelegate_"+str(index): + return (False, ) if tokenListItem != None and tokenListItem.objectName == "AssetView_TokenListItem_" + symbol and tokenListItem.balance != "0": return (True, tokenListItem) return (False, ) diff --git a/test/ui-test/testSuites/suite_wallet/dev/.env b/test/ui-test/testSuites/suite_wallet/dev/.env index 7194c2a979..d861ef366a 100644 --- a/test/ui-test/testSuites/suite_wallet/dev/.env +++ b/test/ui-test/testSuites/suite_wallet/dev/.env @@ -1,3 +1,3 @@ GANACHE_RPC_PORT=9545 GANACHE_MNEMONIC='pelican chief sudden oval media rare swamp elephant lawsuit wheat knife initial' -GANACHE_DB_FOLDER="./../../../../../test/ui-test/fixtures/ganache-dbs/goerli" \ No newline at end of file +GANACHE_DB_FOLDER=./../../../../../test/ui-test/fixtures/ganache-dbs/goerli diff --git a/test/ui-test/testSuites/suite_wallet/tst_transaction/test.feature b/test/ui-test/testSuites/suite_wallet/tst_transaction/test.feature index d27bbca410..087fb81e68 100644 --- a/test/ui-test/testSuites/suite_wallet/tst_transaction/test.feature +++ b/test/ui-test/testSuites/suite_wallet/tst_transaction/test.feature @@ -21,7 +21,7 @@ Feature: Status Desktop Transaction Examples: | amount | token | chain_name | - | 0.1 | ETH | Ethereum Mainnet | + | 1 | ETH | Ethereum Mainnet | # | 1 | ETH | Goerli | # | 1 | STT | Goerli | # | 100 | STT | Goerli | diff --git a/ui/StatusQ/src/StatusQ/Components/StatusListItem.qml b/ui/StatusQ/src/StatusQ/Components/StatusListItem.qml index 55af5a6683..1c23529705 100644 --- a/ui/StatusQ/src/StatusQ/Components/StatusListItem.qml +++ b/ui/StatusQ/src/StatusQ/Components/StatusListItem.qml @@ -33,6 +33,7 @@ Rectangle { property var inlineTagModel: [] property Component inlineTagDelegate property bool loading: false + property bool errorMode: false property StatusAssetSettings asset: StatusAssetSettings { height: isImage ? 40 : 20 @@ -78,6 +79,7 @@ Rectangle { property alias statusListItemInlineTagsSlot: statusListItemTagsSlotInline property alias statusListItemLabel: statusListItemLabel property alias subTitleBadgeComponent: subTitleBadgeLoader.sourceComponent + property alias errorIcon: errorIcon signal clicked(string itemId, var mouse) signal titleClicked(string titleId) @@ -251,11 +253,24 @@ Rectangle { anchors.leftMargin: 4 } + StatusFlatRoundButton { + id: errorIcon + anchors.top: statusListItemTitle.bottom + width: 14 + height: visible ? 14 : 0 + icon.width: 14 + icon.height: 14 + icon.name: "tiny/warning" + icon.color: Theme.palette.dangerColor1 + visible: root.errorMode && !!toolTip.text + } + RowLayout { id: statusListItemSubtitleTagsRow anchors.top: statusListItemTitle.bottom width: parent.width spacing: 4 + visible: !errorMode Loader { id: subTitleBadgeLoader diff --git a/ui/app/AppLayouts/Wallet/controls/FooterTooltipButton.qml b/ui/app/AppLayouts/Wallet/controls/FooterTooltipButton.qml new file mode 100644 index 0000000000..f52b8a53ed --- /dev/null +++ b/ui/app/AppLayouts/Wallet/controls/FooterTooltipButton.qml @@ -0,0 +1,29 @@ +import QtQuick 2.14 + +import StatusQ.Controls 0.1 + +Item { + property alias button: button + property alias text: button.text + property alias icon: button.icon.name + property alias tooltipText: tooltip.text + + implicitWidth: button.width + implicitHeight: button.height + + StatusFlatButton { + id: button + anchors.centerIn: parent + } + MouseArea { + id: mouseArea + anchors.fill: button + hoverEnabled: !button.enabled + enabled: !button.enabled + cursorShape: Qt.PointingHandCursor + } + StatusToolTip { + id: tooltip + visible: mouseArea.containsMouse + } +} diff --git a/ui/app/AppLayouts/Wallet/panels/WalletFooter.qml b/ui/app/AppLayouts/Wallet/panels/WalletFooter.qml index 27e6198973..a3e019d45c 100644 --- a/ui/app/AppLayouts/Wallet/panels/WalletFooter.qml +++ b/ui/app/AppLayouts/Wallet/panels/WalletFooter.qml @@ -9,12 +9,14 @@ import StatusQ.Core.Theme 0.1 import utils 1.0 import "../popups" +import "../controls" Rectangle { id: walletFooter property var sendModal property var walletStore + property var networkConnectionStore height: 61 color: Theme.palette.statusAppLayout.rightPanelBackgroundColor @@ -29,13 +31,15 @@ Rectangle { height: parent.height spacing: Style.current.padding - StatusFlatButton { - objectName: "walletFooterSendButton" - icon.name: "send" - text: qsTr("Send") - onClicked: function() { + FooterTooltipButton { + button.objectName: "walletFooterSendButton" + button.icon.name: "send" + button.text: qsTr("Send") + button.enabled: networkConnectionStore.sendBuyBridgeEnabled + button.onClicked: function() { sendModal.open() } + tooltipText: networkConnectionStore.sendBuyBridgeToolTipText } StatusFlatButton { @@ -46,14 +50,15 @@ Rectangle { } } - StatusFlatButton { - id: bridgeBtn - icon.name: "bridge" - text: qsTr("Bridge") - onClicked: function () { + FooterTooltipButton { + button.icon.name: "bridge" + button.text: qsTr("Bridge") + button.enabled: networkConnectionStore.sendBuyBridgeEnabled + button.onClicked: function() { sendModal.isBridgeTx = true sendModal.open() } + tooltipText: networkConnectionStore.sendBuyBridgeToolTipText } StatusFlatButton { diff --git a/ui/app/AppLayouts/Wallet/panels/WalletHeader.qml b/ui/app/AppLayouts/Wallet/panels/WalletHeader.qml index cb6a12ee1f..29a524266a 100644 --- a/ui/app/AppLayouts/Wallet/panels/WalletHeader.qml +++ b/ui/app/AppLayouts/Wallet/panels/WalletHeader.qml @@ -18,6 +18,7 @@ import "../stores" Item { id: root + property var networkConnectionStore property string currency: "" property var currentAccount property var store @@ -47,6 +48,7 @@ Item { customColor: Theme.palette.baseColor1 text: LocaleUtils.currencyAmountToLocaleString(root.currentAccount.currencyBalance) loading: root.walletStore.tokensLoading + visible: !networkConnectionStore.tokenBalanceNotAvailable } } diff --git a/ui/app/AppLayouts/Wallet/views/AssetsDetailView.qml b/ui/app/AppLayouts/Wallet/views/AssetsDetailView.qml index 83cec5dcad..4de54b9363 100644 --- a/ui/app/AppLayouts/Wallet/views/AssetsDetailView.qml +++ b/ui/app/AppLayouts/Wallet/views/AssetsDetailView.qml @@ -21,6 +21,7 @@ Item { id: root property var token: ({}) + property var networkConnectionStore /*required*/ property string address: "" QtObject { @@ -51,10 +52,12 @@ Item { width: parent.width asset.name: token && token.symbol ? Style.png("tokens/%1".arg(token.symbol)) : "" asset.isImage: true - primaryText: token.name ?? "" - secondaryText: token ? LocaleUtils.currencyAmountToLocaleString(token.enabledNetworkBalance) : "" - tertiaryText: token ? LocaleUtils.currencyAmountToLocaleString(token.enabledNetworkCurrencyBalance) : "" + primaryText: token.name ?? Constants.dummyText + secondaryText: token ? LocaleUtils.currencyAmountToLocaleString(token.enabledNetworkBalance) : Constants.dummyText + tertiaryText: token ? LocaleUtils.currencyAmountToLocaleString(token.enabledNetworkCurrencyBalance) : Constants.dummyText balances: token && token.balances ? token.balances : null + isLoading: RootStore.tokensLoading + errorTooltipText: token && token.balances ? networkConnectionStore.getNetworkDownTextForToken(token.balances): "" getNetworkColor: function(chainId){ return RootStore.getNetworkColor(chainId) } @@ -274,17 +277,20 @@ Item { InformationTile { maxWidth: parent.width primaryText: qsTr("Market Cap") - secondaryText: token && token.marketCap ? LocaleUtils.currencyAmountToLocaleString(token.marketCap) : "---" + secondaryText: token && token.marketCap ? LocaleUtils.currencyAmountToLocaleString(token.marketCap) : Constants.dummyText + isLoading: RootStore.tokensLoading } InformationTile { maxWidth: parent.width primaryText: qsTr("Day Low") - secondaryText: token && token.lowDay ? LocaleUtils.currencyAmountToLocaleString(token.lowDay) : "---" + secondaryText: token && token.lowDay ? LocaleUtils.currencyAmountToLocaleString(token.lowDay) : Constants.dummyText + isLoading: RootStore.tokensLoading } InformationTile { maxWidth: parent.width primaryText: qsTr("Day High") - secondaryText: token && token.highDay ? LocaleUtils.currencyAmountToLocaleString(token.highDay) : "---" + secondaryText: token && token.highDay ? LocaleUtils.currencyAmountToLocaleString(token.highDay) : Constants.dummyText + isLoading: RootStore.tokensLoading } Item { Layout.fillWidth: true @@ -293,28 +299,31 @@ Item { readonly property double changePctHour: token.changePctHour ?? 0 maxWidth: parent.width primaryText: qsTr("Hour") - secondaryText: changePctHour ? "%1%".arg(LocaleUtils.numberToLocaleString(changePctHour, 2)) : "---" - secondaryLabel.color: Math.sign(changePctHour) === 0 ? Theme.palette.directColor1 : - Math.sign(changePctHour) === -1 ? Theme.palette.dangerColor1 : - Theme.palette.successColor1 + secondaryText: changePctHour ? "%1%".arg(LocaleUtils.numberToLocaleString(changePctHour, 2)) : Constants.dummyText + secondaryLabel.customColor: changePctHour === 0 ? Theme.palette.directColor1 : + changePctHour < 0 ? Theme.palette.dangerColor1 : + Theme.palette.successColor1 + isLoading: RootStore.tokensLoading } InformationTile { readonly property double changePctDay: token.changePctDay ?? 0 maxWidth: parent.width primaryText: qsTr("Day") - secondaryText: changePctDay ? "%1%".arg(LocaleUtils.numberToLocaleString(changePctDay, 2)) : "---" - secondaryLabel.color: Math.sign(changePctDay) === 0 ? Theme.palette.directColor1 : - Math.sign(changePctDay) === -1 ? Theme.palette.dangerColor1 : - Theme.palette.successColor1 + secondaryText: changePctDay ? "%1%".arg(LocaleUtils.numberToLocaleString(changePctDay, 2)) : Constants.dummyText + secondaryLabel.customColor: changePctDay === 0 ? Theme.palette.directColor1 : + changePctDay < 0 ? Theme.palette.dangerColor1 : + Theme.palette.successColor1 + isLoading: RootStore.tokensLoading } InformationTile { readonly property double changePct24hour: token.changePct24hour ?? 0 maxWidth: parent.width primaryText: qsTr("24 Hours") - secondaryText: changePct24hour ? "%1%".arg(LocaleUtils.numberToLocaleString(changePct24hour, 2)) : "---" - secondaryLabel.color: Math.sign(changePct24hour) === 0 ? Theme.palette.directColor1 : - Math.sign(changePct24hour) === -1 ? Theme.palette.dangerColor1 : - Theme.palette.successColor1 + secondaryText: changePct24hour ? "%1%".arg(LocaleUtils.numberToLocaleString(changePct24hour, 2)) : Constants.dummyText + secondaryLabel.customColor: changePct24hour === 0 ? Theme.palette.directColor1 : + changePct24hour < 0 ? Theme.palette.dangerColor1 : + Theme.palette.successColor1 + isLoading: RootStore.tokensLoading } } @@ -348,18 +357,19 @@ Item { spacing: 24 width: scrollView.availableWidth - StatusBaseText { + StatusTextWithLoadingState { id: tokenDescriptionText width: Math.max(536 , scrollView.availableWidth - tagsLayout.width - 24) font.pixelSize: 15 lineHeight: 22 lineHeightMode: Text.FixedHeight - text: token.description ?? "" - color: Theme.palette.directColor1 + text: token.description ?? Constants.dummyText + customColor: Theme.palette.directColor1 elide: Text.ElideRight wrapMode: Text.Wrap textFormat: Qt.RichText + loading: RootStore.tokensLoading } ColumnLayout { id: tagsLayout diff --git a/ui/app/AppLayouts/Wallet/views/LeftTabView.qml b/ui/app/AppLayouts/Wallet/views/LeftTabView.qml index f569780729..bff41bca99 100644 --- a/ui/app/AppLayouts/Wallet/views/LeftTabView.qml +++ b/ui/app/AppLayouts/Wallet/views/LeftTabView.qml @@ -14,6 +14,7 @@ import shared 1.0 import shared.panels 1.0 import shared.controls 1.0 import shared.popups.keycard 1.0 +import shared.stores 1.0 import "../controls" import "../popups" @@ -22,6 +23,7 @@ import "../stores" Rectangle { id: root + readonly property NetworkConnectionStore networkConnectionStore: NetworkConnectionStore {} property var changeSelectedAccount: function(){} property bool showSavedAddresses: false onShowSavedAddressesChanged: { @@ -117,6 +119,20 @@ Rectangle { font.weight: Font.Medium font.pixelSize: 22 loading: RootStore.tokensLoading + visible: !networkConnectionStore.tokenBalanceNotAvailable + } + + StatusFlatRoundButton { + id: errorIcon + width: 14 + height: visible ? 14 : 0 + icon.width: 14 + icon.height: 14 + icon.name: "tiny/warning" + icon.color: Theme.palette.dangerColor1 + tooltip.text: networkConnectionStore.tokenBalanceNotAvailableText + tooltip.maxWidth: 200 + visible: networkConnectionStore.tokenBalanceNotAvailable } StatusBaseText { @@ -162,6 +178,9 @@ Rectangle { statusListItemTitle.font.weight: Font.Medium color: sensor.containsMouse || highlighted ? Theme.palette.baseColor3 : "transparent" statusListItemSubTitle.loading: RootStore.tokensLoading + errorMode: networkConnectionStore.tokenBalanceNotAvailable + errorIcon.tooltip.maxWidth: 300 + errorIcon.tooltip.text: networkConnectionStore.tokenBalanceNotAvailableText onClicked: { changeSelectedAccount(index) showSavedAddresses = false diff --git a/ui/app/AppLayouts/Wallet/views/RightTabView.qml b/ui/app/AppLayouts/Wallet/views/RightTabView.qml index 84a56a3fb0..cf764590d9 100644 --- a/ui/app/AppLayouts/Wallet/views/RightTabView.qml +++ b/ui/app/AppLayouts/Wallet/views/RightTabView.qml @@ -6,6 +6,7 @@ import StatusQ.Controls 0.1 import utils 1.0 import shared.views 1.0 +import shared.stores 1.0 import "./" import "../stores" @@ -19,6 +20,7 @@ Item { property var store property var contactsStore property var sendModal + readonly property NetworkConnectionStore networkConnectionStore: NetworkConnectionStore {} function resetStack() { stack.currentIndex = 0; @@ -58,6 +60,7 @@ Item { currentAccount: RootStore.currentAccount store: root.store walletStore: RootStore + networkConnectionStore: root.networkConnectionStore } StatusTabBar { id: walletTabBar @@ -90,6 +93,7 @@ Item { AssetsView { account: RootStore.currentAccount + networkConnectionStore: root.networkConnectionStore assetDetailsLaunched: stack.currentIndex === 2 onAssetClicked: { assetDetailView.token = token @@ -124,6 +128,7 @@ Item { visible: (stack.currentIndex === 2) address: RootStore.currentAccount.mixedcaseAddress + networkConnectionStore: root.networkConnectionStore } TransactionDetailView { id: transactionDetailView @@ -142,6 +147,7 @@ Item { Layout.rightMargin: !!root.StackView ? -root.StackView.view.anchors.rightMargin : 0 sendModal: root.sendModal walletStore: RootStore + networkConnectionStore: root.networkConnectionStore } } } diff --git a/ui/app/AppLayouts/stores/RootStore.qml b/ui/app/AppLayouts/stores/RootStore.qml index a79e37c73e..daf221edf1 100644 --- a/ui/app/AppLayouts/stores/RootStore.qml +++ b/ui/app/AppLayouts/stores/RootStore.qml @@ -156,29 +156,4 @@ QtObject { function resolveENS(value) { mainModuleInst.resolveENS(value, "") } - - property var networkConnectionModuleInst: networkConnectionModule - - function getChainIdsJointString(chainIdsDown) { - let jointChainIdString = "" - for (const chain of chainIdsDown) { - jointChainIdString = (!!jointChainIdString) ? jointChainIdString + " & " : jointChainIdString - jointChainIdString += allNetworks.getNetworkFullName(parseInt(chain)) - } - return jointChainIdString - } - - function retryConnection(websiteDown) { - switch(websiteDown) { - case Constants.walletConnections.blockchains: - networkConnectionModule.refreshBlockchainValues() - break - case Constants.walletConnections.market: - networkConnectionModule.refreshMarketValues() - break - case Constants.walletConnections.collectibles: - networkConnectionModule.refreshCollectiblesValues() - break - } - } } diff --git a/ui/app/mainui/AppMain.qml b/ui/app/mainui/AppMain.qml index f390fa1cc0..d60c1ffa77 100644 --- a/ui/app/mainui/AppMain.qml +++ b/ui/app/mainui/AppMain.qml @@ -680,7 +680,6 @@ Item { ConnectionWarnings { id: walletBlockchainConnectionBanner objectName: "walletBlockchainConnectionBanner" - readonly property string jointChainIdString: appMain.rootStore.getChainIdsJointString(chainIdsDown) Layout.fillWidth: true websiteDown: Constants.walletConnections.blockchains text: { @@ -723,6 +722,7 @@ Item { StatusToolTip { id: toolTip orientation: StatusToolTip.Orientation.Bottom + maxWidth: 300 } } diff --git a/ui/imports/shared/controls/AssetsDetailsHeader.qml b/ui/imports/shared/controls/AssetsDetailsHeader.qml index cc85e7245e..efd219fe07 100644 --- a/ui/imports/shared/controls/AssetsDetailsHeader.qml +++ b/ui/imports/shared/controls/AssetsDetailsHeader.qml @@ -16,6 +16,8 @@ Control { property alias secondaryText: cryptoBalance.text property alias tertiaryText: fiatBalance.text property var balances + property bool isLoading: false + property string errorTooltipText property StatusAssetSettings asset: StatusAssetSettings { width: 40 height: 40 @@ -34,8 +36,9 @@ Control { id: identiconLoader anchors.verticalCenter: parent.verticalCenter asset: root.asset + loading: root.isLoading } - StatusBaseText { + StatusTextWithLoadingState { id: tokenName width: Math.min(root.width - identiconLoader.width - cryptoBalance.width - fiatBalance.width - 24, implicitWidth) anchors.verticalCenter: parent.verticalCenter @@ -43,15 +46,17 @@ Control { lineHeight: 30 lineHeightMode: Text.FixedHeight elide: Text.ElideRight - color: Theme.palette.directColor1 + customColor: Theme.palette.directColor1 + loading: root.isLoading } - StatusBaseText { + StatusTextWithLoadingState { id: cryptoBalance anchors.verticalCenter: parent.verticalCenter font.pixelSize: 22 lineHeight: 30 lineHeightMode: Text.FixedHeight - color: Theme.palette.baseColor1 + customColor: Theme.palette.baseColor1 + loading: root.isLoading } StatusBaseText { id: dotSeparator @@ -61,13 +66,14 @@ Control { color: Theme.palette.baseColor1 text: "." } - StatusBaseText { + StatusTextWithLoadingState { id: fiatBalance anchors.verticalCenter: parent.verticalCenter font.pixelSize: 22 lineHeight: 30 lineHeightMode: Text.FixedHeight - color: Theme.palette.baseColor1 + customColor: Theme.palette.baseColor1 + loading: root.isLoading } } Row { @@ -81,6 +87,17 @@ Control { tagPrimaryLabel.text: root.formatBalance(model.balance) tagPrimaryLabel.color: root.getNetworkColor(model.chainId) image.source: Style.svg("tiny/%1".arg(root.getNetworkIcon(model.chainId))) + loading: root.isLoading + rightComponent: StatusFlatRoundButton { + width: 14 + height: visible ? 14 : 0 + icon.width: 14 + icon.height: 14 + icon.name: "tiny/warning" + icon.color: Theme.palette.dangerColor1 + tooltip.text: root.errorTooltipText + visible: !!root.errorTooltipText + } } } } diff --git a/ui/imports/shared/controls/InformationTile.qml b/ui/imports/shared/controls/InformationTile.qml index 3a9749166b..ec4e73e8ce 100644 --- a/ui/imports/shared/controls/InformationTile.qml +++ b/ui/imports/shared/controls/InformationTile.qml @@ -19,6 +19,7 @@ Rectangle { property alias tagsDelegate: tags.delegate property int maxWidth: 0 property bool copy: false + property bool isLoading: false signal copyClicked(string textToCopy) @@ -43,13 +44,14 @@ Rectangle { } RowLayout { width: 100 - StatusBaseText { + StatusTextWithLoadingState { id: secondaryText Layout.maximumWidth: root.maxWidth - Style.current.xlPadding - (root.copy ? 50 : 0) font.pixelSize: 15 - color: Theme.palette.directColor1 + customColor: Theme.palette.directColor1 visible: text elide: Text.ElideRight + loading: root.isLoading } CopyToClipBoardButton { visible: root.copy diff --git a/ui/imports/shared/controls/TokenDelegate.qml b/ui/imports/shared/controls/TokenDelegate.qml index 75872d2c03..e0c1fe2062 100644 --- a/ui/imports/shared/controls/TokenDelegate.qml +++ b/ui/imports/shared/controls/TokenDelegate.qml @@ -21,24 +21,52 @@ StatusListItem { Math.sign(changePct24hour) === 0 ? Theme.palette.baseColor1 : Math.sign(changePct24hour) === -1 ? Theme.palette.dangerColor1 : Theme.palette.successColor1 + property string errorTooltipText_1 + property string errorTooltipText_2 title: name subTitle: LocaleUtils.currencyAmountToLocaleString(enabledNetworkBalance) asset.name: symbol ? Style.png("tokens/" + symbol) : "" asset.isImage: true + statusListItemTitleIcons.sourceComponent: StatusFlatRoundButton { + width: 14 + height: visible ? 14 : 0 + icon.width: 14 + icon.height: 14 + icon.name: "tiny/warning" + icon.color: Theme.palette.dangerColor1 + tooltip.text: root.errorTooltipText_1 + tooltip.maxWidth: 300 + visible: !!tooltip.text + } + components: [ Column { id: valueColumn + StatusFlatRoundButton { + id: errorIcon + width: 14 + height: visible ? 14 : 0 + icon.width: 14 + icon.height: 14 + icon.name: "tiny/warning" + icon.color: Theme.palette.dangerColor1 + tooltip.text: root.errorTooltipText_2 + tooltip.maxWidth: 200 + visible: !!tooltip.text + } StatusTextWithLoadingState { id: localeCurrencyBalance anchors.right: parent.right font.pixelSize: 15 text: LocaleUtils.currencyAmountToLocaleString(enabledNetworkCurrencyBalance) + visible: !errorIcon.visible } Row { anchors.horizontalCenter: parent.horizontalCenter spacing: 8 + visible: !errorIcon.visible StatusTextWithLoadingState { id: change24HourText font.pixelSize: 15 diff --git a/ui/imports/shared/panels/ConnectionWarnings.qml b/ui/imports/shared/panels/ConnectionWarnings.qml index d2ad896956..09c027e1d7 100644 --- a/ui/imports/shared/panels/ConnectionWarnings.qml +++ b/ui/imports/shared/panels/ConnectionWarnings.qml @@ -3,10 +3,13 @@ import QtQuick 2.14 import StatusQ.Core 0.1 import utils 1.0 +import shared.stores 1.0 ModuleWarning { id: root + readonly property NetworkConnectionStore networkConnectionStore: NetworkConnectionStore {} + readonly property string jointChainIdString: networkConnectionStore.getChainIdsJointString(chainIdsDown) property string websiteDown property int connectionState: -1 property int autoTryTimerInSecs: 0 @@ -37,11 +40,11 @@ ModuleWarning { type: connectionState === Constants.ConnectionStatus.Success ? ModuleWarning.Success : ModuleWarning.Danger buttonText: connectionState === Constants.ConnectionStatus.Failure ? qsTr("Retry now") : "" - onClicked: appMain.rootStore.retryConnection(websiteDown) + onClicked: networkConnectionStore.retryConnection(websiteDown) onCloseClicked: hide() Connections { - target: appMain.rootStore.networkConnectionModuleInst + target: networkConnectionStore.networkConnectionModuleInst function onNetworkConnectionStatusUpdate(website: string, completelyDown: bool, connectionState: int, chainIds: string, lastCheckedAt: int, timeToAutoRetryInSecs: int, withCache: bool) { if (website === websiteDown) { root.connectionState = connectionState diff --git a/ui/imports/shared/popups/SendModal.qml b/ui/imports/shared/popups/SendModal.qml index 7c5ac3483e..0cd1e96e18 100644 --- a/ui/imports/shared/popups/SendModal.qml +++ b/ui/imports/shared/popups/SendModal.qml @@ -120,6 +120,7 @@ StatusDialog { popup.recalculateRoutesAndFees() } } + readonly property NetworkConnectionStore networkConnectionStore: NetworkConnectionStore {} onErrorTypeChanged: { if(errorType === Constants.SendAmountExceedsBalance) @@ -166,6 +167,13 @@ StatusDialog { recipientSelector.input.text = popup.selectedAccount.address d.waitTimer.restart() } + + // add networks that are down to disabled list + if(!!d.networkConnectionStore.blockchainNetworksDown) { + for(let i in d.networkConnectionStore.blockchainNetworksDown) { + store.addRemoveDisabledToChain(parseInt(d.networkConnectionStore.blockchainNetworksDown[i]), true) + } + } } onClosed: popup.store.resetTxStoreProperties() diff --git a/ui/imports/shared/stores/NetworkConnectionStore.qml b/ui/imports/shared/stores/NetworkConnectionStore.qml new file mode 100644 index 0000000000..ccbb8a6c28 --- /dev/null +++ b/ui/imports/shared/stores/NetworkConnectionStore.qml @@ -0,0 +1,95 @@ +import QtQuick 2.13 + +import StatusQ.Core 0.1 + +import utils 1.0 + +QtObject { + id: root + + readonly property var networkConnectionModuleInst: networkConnectionModule + + readonly property var blockchainNetworksDown: networkConnectionModule.blockchainNetworkConnection.chainIds.split(";") + readonly property bool atleastOneBlockchainNetworkAvailable: blockchainNetworksDown.length < networksModule.all.count + readonly property bool noBlockchainConnWithoutCache: (!mainModule.isOnline || networkConnectionModule.blockchainNetworkConnection.completelyDown) && + !walletSection.tokensLoading && walletSectionCurrent.assets.count === 0 + + readonly property bool sendBuyBridgeEnabled: localAppSettings.testEnvironment || (mainModule.isOnline && + (!networkConnectionModule.blockchainNetworkConnection.completelyDown && atleastOneBlockchainNetworkAvailable) && + !networkConnectionModule.marketValuesNetworkConnection.completelyDown) + readonly property string sendBuyBridgeToolTipText: !mainModule.isOnline ? + qsTr("Requires internet connection") : + networkConnectionModule.blockchainNetworkConnection.completelyDown || + (!networkConnectionModule.blockchainNetworkConnection.completelyDown && + !atleastOneBlockchainNetworkAvailable) ? + qsTr("Requires Pocket Network(POKT) or Infura, both of which are currently unavailable") : + networkConnectionModule.marketValuesNetworkConnection.completelyDown ? + qsTr("Requires CryptoCompare or CoinGecko, both of which are currently unavailable"): + qsTr("Requires POKT/ Infura and CryptoCompare/CoinGecko, which are all currently unavailable") + + + readonly property bool tokenBalanceNotAvailable: ((!mainModule.isOnline || networkConnectionModule.blockchainNetworkConnection.completelyDown) && + !walletSection.tokensLoading && walletSectionCurrent.assets.count === 0) || + (networkConnectionModule.marketValuesNetworkConnection.completelyDown && + !networkConnectionModule.marketValuesNetworkConnection.withCache) + readonly property string tokenBalanceNotAvailableText: !mainModule.isOnline ? + qsTr("Internet connection lost. Data could not be retrieved.") : + networkConnectionModule.blockchainNetworkConnection.completelyDown ? + qsTr("Token balances are fetched from Pocket Network (POKT) and Infura which are both curently unavailable") : + networkConnectionModule.marketValuesNetworkConnection.completelyDown ? + qsTr("Market values are fetched from CryptoCompare and CoinGecko which are both currently unavailable") : + qsTr("Market values and token balances use CryptoCompare/CoinGecko and POKT/Infura which are all currently unavailable.") + + function getBlockchainNetworkDownTextForToken(balances) { + if(!!balances && !networkConnectionModule.blockchainNetworkConnection.completelyDown) { + let chainIdsDown = [] + for (var i =0; i 0) { + return qsTr("Pocket Network (POKT) & Infura are currently both unavailable for %1. %1 balances are as of %2.") + .arg(getChainIdsJointString(chainIdsDown)) + .arg(LocaleUtils.formatDateTime(new Date(networkConnectionModule.blockchainNetworkConnection.lastCheckedAt*1000))) + } + } + return "" + } + + function getMarketNetworkDownText() { + if(networkConnectionModule.blockchainNetworkConnection.completelyDown && + !walletSection.tokensLoading && walletSectionCurrent.assets.count === 0 && + networkConnectionModule.marketValuesNetworkConnection.completelyDown && + !networkConnectionModule.marketValuesNetworkConnection.withCache) + return qsTr("Market values and token balances use CryptoCompare/CoinGecko and POKT/Infura which are all currently unavailable.") + else if(networkConnectionModule.marketValuesNetworkConnection.completelyDown && + !networkConnectionModule.marketValuesNetworkConnection.withCache) + return qsTr("Market values are fetched from CryptoCompare and CoinGecko which are both currently unavailable") + else + return "" + } + + function getChainIdsJointString(chainIdsDown) { + let jointChainIdString = "" + for (const chain of chainIdsDown) { + jointChainIdString = (!!jointChainIdString) ? jointChainIdString + " & " : jointChainIdString + jointChainIdString += networksModule.all.getNetworkFullName(parseInt(chain)) + } + return jointChainIdString + } + + function retryConnection(websiteDown) { + switch(websiteDown) { + case Constants.walletConnections.blockchains: + networkConnectionModule.refreshBlockchainValues() + break + case Constants.walletConnections.market: + networkConnectionModule.refreshMarketValues() + break + case Constants.walletConnections.collectibles: + networkConnectionModule.refreshCollectiblesValues() + break + } + } +} diff --git a/ui/imports/shared/stores/qmldir b/ui/imports/shared/stores/qmldir index fdf3ca8af4..760852646f 100644 --- a/ui/imports/shared/stores/qmldir +++ b/ui/imports/shared/stores/qmldir @@ -5,3 +5,4 @@ BIP39_en 1.0 BIP39_en.qml ChartStoreBase 1.0 ChartStoreBase.qml TokenBalanceHistoryStore 1.0 TokenBalanceHistoryStore.qml TokenMarketValuesStore 1.0 TokenMarketValuesStore.qml +NetworkConnectionStore 1.0 NetworkConnectionStore.qml diff --git a/ui/imports/shared/views/AssetsView.qml b/ui/imports/shared/views/AssetsView.qml index f2a00df573..02f5ea2cdd 100644 --- a/ui/imports/shared/views/AssetsView.qml +++ b/ui/imports/shared/views/AssetsView.qml @@ -17,6 +17,7 @@ Item { id: root property var account + property var networkConnectionStore property bool assetDetailsLaunched: false signal assetClicked(var token) @@ -32,8 +33,9 @@ Item { id: assetListView objectName: "assetViewStatusListView" anchors.fill: parent - model: RootStore.tokensLoading ? Constants.dummyModelItems : filteredModel - delegate: RootStore.tokensLoading ? loadingTokenDelegate : tokenDelegate + // To-do: will try to move the loading tokens to the nim side under this task https://github.com/status-im/status-desktop/issues/9648 + model: RootStore.tokensLoading || networkConnectionStore.noBlockchainConnWithoutCache ? Constants.dummyModelItems : filteredModel + delegate: RootStore.tokensLoading || networkConnectionStore.noBlockchainConnWithoutCache ? loadingTokenDelegate : tokenDelegate } SortFilterProxyModel { @@ -49,6 +51,7 @@ Item { Component { id: loadingTokenDelegate LoadingTokenDelegate { + objectName: "AssetView_LoadingTokenDelegate_" + index width: ListView.view.width } } @@ -58,6 +61,8 @@ Item { TokenDelegate { objectName: "AssetView_TokenListItem_" + symbol readonly property string balance: "%1".arg(enabledNetworkBalance.amount) // Needed for the tests + errorTooltipText_1: networkConnectionStore.getBlockchainNetworkDownTextForToken(balances) + errorTooltipText_2: networkConnectionStore.getMarketNetworkDownText() width: ListView.view.width onClicked: { RootStore.getHistoricalDataForToken(symbol, RootStore.currencyStore.currentCurrency)