Skip to content

Commit

Permalink
✨(lld): change sync error tooltip to be more readable
Browse files Browse the repository at this point in the history
  • Loading branch information
LucasWerey committed Oct 18, 2024
1 parent b1ac76b commit b1d44e1
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 52 deletions.
6 changes: 6 additions & 0 deletions .changeset/long-meals-laugh.md
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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(() => {
Expand All @@ -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 <IconExclamationCircle size={16} />;
if (isRotating) return <IconLoader size={16} />;
if (areAllAccountsUpToDate) return <IconCheckCircle size={16} />;
return <IconExclamationCircle size={16} />;
};

const getText = () => {
if (isRotating) return <Trans i18nKey="common.sync.syncing" />;
if (isError) {
return (
<>
<Box>
<Trans i18nKey="common.sync.error" />
</Box>
<Box
ml={2}
style={{
textDecoration: "underline",
pointerEvents: "all",
cursor: "pointer",
}}
onClick={onClick}
>
<Trans i18nKey="common.sync.refresh" />
</Box>
</>
);
}
if (areAllAccountsUpToDate) {
return (
<span data-testid="topbar-synchronized">
<Trans i18nKey="common.sync.upToDate" />
</span>
);
}
return <Trans i18nKey="common.sync.outdated" />;
};

const content = (
<ItemContainer
data-testid="topbar-synchronize-button"
Expand All @@ -60,20 +123,12 @@ export default function ActivityIndicatorInner() {
? "alertRed"
: isRotating
? "palette.text.shade60"
: isUpToDate
: areAllAccountsUpToDate
? "positiveGreen"
: "palette.text.shade60"
}
>
{isError ? (
<IconExclamationCircle size={16} />
) : isRotating ? (
<IconLoader size={16} />
) : isUpToDate ? (
<IconCheckCircle size={16} />
) : (
<IconExclamationCircle size={16} />
)}
{getIcon()}
</Rotating>
<Box
ml={isRotating ? 2 : 1}
Expand All @@ -83,36 +138,12 @@ export default function ActivityIndicatorInner() {
horizontal
alignItems="center"
>
{isRotating ? (
<Trans i18nKey="common.sync.syncing" />
) : isError ? (
<>
<Box>
<Trans i18nKey="common.sync.error" />
</Box>
<Box
ml={2}
style={{
textDecoration: "underline",
pointerEvents: "all",
cursor: "pointer",
}}
onClick={onClick}
>
<Trans i18nKey="common.sync.refresh" />
</Box>
</>
) : isUpToDate ? (
<span data-testid="topbar-synchronized">
<Trans i18nKey="common.sync.upToDate" />
</span>
) : (
<Trans i18nKey="common.sync.outdated" />
)}
{getText()}
</Box>
</ItemContainer>
);
if (isError && error) {

if (!areAllAccountsUpToDate || (isError && error)) {
return (
<Tooltip
tooltipBg="alertRed"
Expand All @@ -124,13 +155,29 @@ export default function ActivityIndicatorInner() {
maxWidth: 250,
}}
>
<TranslatedError error={error} />
{isError && error && areAllAccountsUpToDate ? (
<TranslatedError error={error} />
) : (
<Box>
<Text mb={1} textAlign="left">
{"Accounts failing to sync:"}
</Text>
{allAccountNamesWithSyncError.map((accountName, index) => (
<Text key={index} textAlign="left">
<li>{accountName}</li>
</Text>
))}
</Box>
)}
</Box>
}
>
{content}
</Tooltip>
);
}

return content;
}
};

export default ActivityIndicatorInner;
15 changes: 9 additions & 6 deletions apps/ledger-live-desktop/src/renderer/reducers/accounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
}),
);

Expand Down
9 changes: 9 additions & 0 deletions apps/ledger-live-desktop/src/renderer/reducers/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
31 changes: 29 additions & 2 deletions libs/ledger-live-common/src/bridge/react/useAccountSyncState.ts
Original file line number Diff line number Diff line change
@@ -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,
}: {
Expand All @@ -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[]);
}

0 comments on commit b1d44e1

Please sign in to comment.