From b1d44e1c9784140127f5255927230dadb3688a12 Mon Sep 17 00:00:00 2001 From: Lucas Werey Date: Fri, 18 Oct 2024 13:27:25 +0200 Subject: [PATCH] :sparkles:(lld): change sync error tooltip to be more readable --- .changeset/long-meals-laugh.md | 6 + .../components/TopBar/ActivityIndicator.tsx | 135 ++++++++++++------ .../src/renderer/reducers/accounts.ts | 15 +- .../src/renderer/reducers/wallet.ts | 9 ++ .../src/bridge/react/useAccountSyncState.ts | 31 +++- 5 files changed, 144 insertions(+), 52 deletions(-) create mode 100644 .changeset/long-meals-laugh.md diff --git a/.changeset/long-meals-laugh.md b/.changeset/long-meals-laugh.md new file mode 100644 index 000000000000..45be9c142b63 --- /dev/null +++ b/.changeset/long-meals-laugh.md @@ -0,0 +1,6 @@ +--- +"ledger-live-desktop": patch +"@ledgerhq/live-common": patch +--- + +Update the ui of the tooltip when there is some sync errors. We now display the name of the accounts that failed to sync diff --git a/apps/ledger-live-desktop/src/renderer/components/TopBar/ActivityIndicator.tsx b/apps/ledger-live-desktop/src/renderer/components/TopBar/ActivityIndicator.tsx index ca9c3be61b6a..6deed57f567a 100644 --- a/apps/ledger-live-desktop/src/renderer/components/TopBar/ActivityIndicator.tsx +++ b/apps/ledger-live-desktop/src/renderer/components/TopBar/ActivityIndicator.tsx @@ -1,7 +1,11 @@ import React, { useState, useCallback } from "react"; import { useSelector } from "react-redux"; import { Trans } from "react-i18next"; -import { useBridgeSync, useGlobalSyncState } from "@ledgerhq/live-common/bridge/react/index"; +import { + useBatchAccountsSyncState, + useBridgeSync, + useGlobalSyncState, +} from "@ledgerhq/live-common/bridge/react/index"; import { useCountervaluesPolling } from "@ledgerhq/live-countervalues-react"; import { getEnv } from "@ledgerhq/live-env"; import { track } from "~/renderer/analytics/segment"; @@ -15,18 +19,37 @@ import TranslatedError from "../TranslatedError"; import Box from "../Box"; import { ItemContainer } from "./shared"; import { useWalletSyncUserState } from "LLD/features/WalletSync/components/WalletSyncContext"; +import { useBatchMaybeAccountName } from "~/renderer/reducers/wallet"; +import { getDefaultAccountName } from "@ledgerhq/live-wallet/accountName"; +import { Text } from "@ledgerhq/react-ui"; -export default function ActivityIndicatorInner() { +const ActivityIndicatorInner = () => { const wsUserState = useWalletSyncUserState(); const bridgeSync = useBridgeSync(); const globalSyncState = useGlobalSyncState(); const isUpToDate = useSelector(isUpToDateSelector); const cvPolling = useCountervaluesPolling(); + + const allAccounts = isUpToDate.map(ojb => ojb.account); + const allAccountsWithSyncProblem = useBatchAccountsSyncState({ accounts: allAccounts }).filter( + ({ syncState, account }) => + !syncState.pending && + (syncState.error || !isUpToDate.find(obj => obj.account.id === account.id)?.isUpToDate), + ); + const allMaybeAccountNames = useBatchMaybeAccountName( + allAccountsWithSyncProblem.map(ojb => ojb.account), + ); + const allAccountNamesWithSyncError = allMaybeAccountNames.map( + (name, index) => name ?? getDefaultAccountName(allAccountsWithSyncProblem[index].account), + ); + + const areAllAccountsUpToDate = allAccountNamesWithSyncError.length === 0; + const isPending = cvPolling.pending || globalSyncState.pending || wsUserState.visualPending; const syncError = !isPending && (cvPolling.error || globalSyncState.error || wsUserState.walletSyncError); - // we only show error if it's not up to date. this hide a bit error that happen from time to time - const isError = (!!syncError && !isUpToDate) || !!wsUserState.walletSyncError; + + const isError = !!syncError || !areAllAccountsUpToDate || !!wsUserState.walletSyncError; const error = (syncError ? globalSyncState.error : null) || wsUserState.walletSyncError; const [lastClickTime, setLastclickTime] = useState(0); const onClick = useCallback(() => { @@ -40,11 +63,51 @@ export default function ActivityIndicatorInner() { setLastclickTime(Date.now()); track("SyncRefreshClick"); }, [cvPolling, bridgeSync, wsUserState]); - const isSpectronRun = getEnv("PLAYWRIGHT_RUN"); // we will keep 'spinning' in spectron case + const isSpectronRun = getEnv("PLAYWRIGHT_RUN"); const userClickTime = isSpectronRun ? 10000 : 1000; const isUserClick = Date.now() - lastClickTime < userClickTime; // time to keep display the spinning on a UI click. const isRotating = isPending && (!isUpToDate || isUserClick); const isDisabled = isError || isRotating; + + const getIcon = () => { + if (isError) return ; + if (isRotating) return ; + if (areAllAccountsUpToDate) return ; + return ; + }; + + const getText = () => { + if (isRotating) return ; + if (isError) { + return ( + <> + + + + + + + + ); + } + if (areAllAccountsUpToDate) { + return ( + + + + ); + } + return ; + }; + const content = ( - {isError ? ( - - ) : isRotating ? ( - - ) : isUpToDate ? ( - - ) : ( - - )} + {getIcon()} - {isRotating ? ( - - ) : isError ? ( - <> - - - - - - - - ) : isUpToDate ? ( - - - - ) : ( - - )} + {getText()} ); - if (isError && error) { + + if (!areAllAccountsUpToDate || (isError && error)) { return ( - + {isError && error && areAllAccountsUpToDate ? ( + + ) : ( + + + {"Accounts failing to sync:"} + + {allAccountNamesWithSyncError.map((accountName, index) => ( + +
  • {accountName}
  • +
    + ))} +
    + )} } > @@ -132,5 +176,8 @@ export default function ActivityIndicatorInner() {
    ); } + return content; -} +}; + +export default ActivityIndicatorInner; diff --git a/apps/ledger-live-desktop/src/renderer/reducers/accounts.ts b/apps/ledger-live-desktop/src/renderer/reducers/accounts.ts index bb391069b0f5..d14f8d08965a 100644 --- a/apps/ledger-live-desktop/src/renderer/reducers/accounts.ts +++ b/apps/ledger-live-desktop/src/renderer/reducers/accounts.ts @@ -117,14 +117,17 @@ export const subAccountByCurrencyOrderedSelector = createSelector( // FIXME we might reboot this idea later! export const activeAccountsSelector = accountsSelector; export const isUpToDateSelector = createSelector(activeAccountsSelector, accounts => - accounts.every(a => { + accounts.map(a => { const { lastSyncDate } = a; const { blockAvgTime } = a.currency; - if (!blockAvgTime) return true; - const outdated = - Date.now() - (lastSyncDate.getTime() || 0) > - blockAvgTime * 1000 + getEnv("SYNC_OUTDATED_CONSIDERED_DELAY"); - return !outdated; + let isUpToDate = true; + if (blockAvgTime) { + const outdated = + Date.now() - (lastSyncDate.getTime() || 0) > + blockAvgTime * 1000 + getEnv("SYNC_OUTDATED_CONSIDERED_DELAY"); + isUpToDate = !outdated; + } + return { account: a, isUpToDate }; }), ); diff --git a/apps/ledger-live-desktop/src/renderer/reducers/wallet.ts b/apps/ledger-live-desktop/src/renderer/reducers/wallet.ts index 036de6a31fe2..1b41ab05a23e 100644 --- a/apps/ledger-live-desktop/src/renderer/reducers/wallet.ts +++ b/apps/ledger-live-desktop/src/renderer/reducers/wallet.ts @@ -38,6 +38,15 @@ export const useMaybeAccountName = ( !account ? undefined : accountNameWithDefaultSelector(state.wallet, account), ); }; +export const useBatchMaybeAccountName = ( + accounts: (AccountLike | null | undefined)[], +): (string | undefined)[] => { + return useSelector((state: State) => + accounts.map(account => + !account ? undefined : accountNameWithDefaultSelector(state.wallet, account), + ), + ); +}; export const useAccountName = (account: AccountLike) => { return useSelector((state: State) => accountNameWithDefaultSelector(state.wallet, account)); diff --git a/libs/ledger-live-common/src/bridge/react/useAccountSyncState.ts b/libs/ledger-live-common/src/bridge/react/useAccountSyncState.ts index 987a9ccfb4c4..e006b55c8fcd 100644 --- a/libs/ledger-live-common/src/bridge/react/useAccountSyncState.ts +++ b/libs/ledger-live-common/src/bridge/react/useAccountSyncState.ts @@ -1,9 +1,12 @@ -import type { SyncState } from "./types"; +import { Account } from "@ledgerhq/types-live"; import { useBridgeSyncState } from "./context"; -const nothingState = { +import type { SyncState } from "./types"; + +const nothingState: SyncState = { pending: false, error: null, }; + export function useAccountSyncState({ accountId, }: { @@ -12,3 +15,27 @@ export function useAccountSyncState({ const syncState = useBridgeSyncState(); return (accountId && syncState[accountId]) || nothingState; } + +interface AccountWithSyncState { + account: Account; + syncState: SyncState; +} + +export function useBatchAccountsSyncState({ + accounts, +}: { + accounts?: (Account | null)[]; +} = {}): AccountWithSyncState[] { + const syncState = useBridgeSyncState(); + if (!accounts) return []; + + return accounts.reduce((acc, account) => { + if (account) { + acc.push({ + account, + syncState: syncState[account.id] || nothingState, + }); + } + return acc; + }, [] as AccountWithSyncState[]); +}