From 22befacb0e202270c95df698af0d358c43af390c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Von=C3=A1=C5=A1ek?= Date: Fri, 17 Apr 2026 17:32:26 +0200 Subject: [PATCH] xc-scan list improvements --- apps/main/src/i18n/locales/en/common.json | 1 + .../MultisigNotification.tsx | 2 +- .../NotificationCenter/NotificationCenter.tsx | 13 +--- apps/main/src/modules/xcm/XcmPage.tsx | 22 +++--- apps/main/src/modules/xcm/XcmPageSkeleton.tsx | 23 +++++++ .../src/modules/xcm/history/XcJourneyCard.tsx | 29 +++++--- .../history/XcScanHistoryTable.columns.tsx | 34 +++++---- .../xcm/history/XcScanJourneyListSkeleton.tsx | 58 ++++++++++++++++ .../modules/xcm/history/XcmHistoryPanel.tsx | 69 +++++++++++++++++++ .../history/hooks/useClaimableTransactions.ts | 17 +++++ .../main/src/modules/xcm/history/useXcScan.ts | 1 + .../src/modules/xcm/history/utils/journey.ts | 2 +- .../xcm/transfer/XcmTransferSkeleton.tsx | 8 ++- apps/main/src/routes/cross-chain/index.tsx | 4 +- .../ExternalLink/ExternalLink.styled.ts | 9 +++ .../components/ExternalLink/ExternalLink.tsx | 16 ++++- packages/ui/src/components/Icon/Icon.tsx | 11 ++- .../ToggleGroup/ToggleGroup.styled.ts | 4 ++ .../components/account/AccountIdentity.tsx | 50 ++++++++++++-- 19 files changed, 312 insertions(+), 61 deletions(-) create mode 100644 apps/main/src/modules/xcm/XcmPageSkeleton.tsx create mode 100644 apps/main/src/modules/xcm/history/XcScanJourneyListSkeleton.tsx create mode 100644 apps/main/src/modules/xcm/history/XcmHistoryPanel.tsx create mode 100644 apps/main/src/modules/xcm/history/hooks/useClaimableTransactions.ts create mode 100644 packages/ui/src/components/ExternalLink/ExternalLink.styled.ts diff --git a/apps/main/src/i18n/locales/en/common.json b/apps/main/src/i18n/locales/en/common.json index ea3ed5c94c..7e1d4f795c 100644 --- a/apps/main/src/i18n/locales/en/common.json +++ b/apps/main/src/i18n/locales/en/common.json @@ -24,6 +24,7 @@ "timeFrame.month_other": "Months", "days": "Days", "all": "All", + "claimable": "Claimable", "from": "From", "to": "To", "protocol": "Protocol", diff --git a/apps/main/src/modules/layout/components/NotificationCenter/MultisigNotification.tsx b/apps/main/src/modules/layout/components/NotificationCenter/MultisigNotification.tsx index c44714e5cf..3a4e8d1704 100644 --- a/apps/main/src/modules/layout/components/NotificationCenter/MultisigNotification.tsx +++ b/apps/main/src/modules/layout/components/NotificationCenter/MultisigNotification.tsx @@ -62,7 +62,7 @@ export const MultisigNotification: React.FC = ({ } actions={ - + diff --git a/apps/main/src/modules/layout/components/NotificationCenter/NotificationCenter.tsx b/apps/main/src/modules/layout/components/NotificationCenter/NotificationCenter.tsx index b9282a1c82..2e0a17c2a9 100644 --- a/apps/main/src/modules/layout/components/NotificationCenter/NotificationCenter.tsx +++ b/apps/main/src/modules/layout/components/NotificationCenter/NotificationCenter.tsx @@ -11,7 +11,6 @@ import { Stack, } from "@galacticcouncil/ui/components" import { safeConvertPublicKeyToSS58 } from "@galacticcouncil/utils" -import { useAccount } from "@galacticcouncil/web3-connect" import { FC } from "react" import { useTranslation } from "react-i18next" @@ -21,25 +20,17 @@ import { MultisigNotification } from "@/modules/layout/components/NotificationCe import { NotificationBadge } from "@/modules/layout/components/NotificationCenter/NotificationBadge" import { NotificationGroup } from "@/modules/layout/components/NotificationCenter/NotificationGroup" import { NotificationToast } from "@/modules/layout/components/NotificationCenter/NotificationToast" -import { usePendingClaimsStore } from "@/modules/xcm/history/hooks/usePendingClaimsStore" -import { useXcScan } from "@/modules/xcm/history/useXcScan" +import { useClaimableTransactions } from "@/modules/xcm/history/hooks/useClaimableTransactions" import { useMultisigContext } from "@/providers/MultisigProvider" import { useToasts } from "@/states/toasts" export const NotificationCenter: FC = () => { const { t } = useTranslation() - const { account } = useAccount() const { totalPendingTxCount, multisigs, pendingTxsByMultisig } = useMultisigContext() const { toasts } = useToasts() - const { data: claimable } = useXcScan(account?.address ?? "", { - claimableOnly: true, - }) - const { pendingCorrelationIds } = usePendingClaimsStore() - const visibleClaimable = claimable.filter( - ({ correlationId }) => !pendingCorrelationIds.includes(correlationId), - ) + const visibleClaimable = useClaimableTransactions() const groups = Object.groupBy(toasts, (toast) => toast.variant === "pending" ? "pending" : "completed", diff --git a/apps/main/src/modules/xcm/XcmPage.tsx b/apps/main/src/modules/xcm/XcmPage.tsx index 79e94a7218..114b4fae4c 100644 --- a/apps/main/src/modules/xcm/XcmPage.tsx +++ b/apps/main/src/modules/xcm/XcmPage.tsx @@ -1,31 +1,35 @@ import { Grid } from "@galacticcouncil/ui/components" import { useAccount } from "@galacticcouncil/web3-connect" +import { useClaimableTransactions } from "@/modules/xcm/history/hooks/useClaimableTransactions" import { useXcScan } from "@/modules/xcm/history/useXcScan" -import { XcScanJourneyList } from "@/modules/xcm/history/XcScanJourneyList" +import { XcmHistoryPanel } from "@/modules/xcm/history/XcmHistoryPanel" +import { XcScanJourneyListSkeleton } from "@/modules/xcm/history/XcScanJourneyListSkeleton" import { XcmTransferApp } from "@/modules/xcm/transfer/XcmTransferApp" export const XcmPage = () => { const { account } = useAccount() const address = account?.address ?? "" - const { data } = useXcScan(address) - const shouldRenderJourneyList = !!account && data.length > 0 + const claimable = useClaimableTransactions() + const { data: all, dataUpdatedAt } = useXcScan(address) + + const isLoading = !!account && dataUpdatedAt === 0 + const isTwoColTemplate = !!account && (all.length > 0 || isLoading) return ( - {shouldRenderJourneyList && ( - + {isLoading && } + {!isLoading && all.length > 0 && ( + )} ) diff --git a/apps/main/src/modules/xcm/XcmPageSkeleton.tsx b/apps/main/src/modules/xcm/XcmPageSkeleton.tsx new file mode 100644 index 0000000000..e2fe610693 --- /dev/null +++ b/apps/main/src/modules/xcm/XcmPageSkeleton.tsx @@ -0,0 +1,23 @@ +import { Grid } from "@galacticcouncil/ui/components" +import { useAccount } from "@galacticcouncil/web3-connect" + +import { XcScanJourneyListSkeleton } from "@/modules/xcm/history/XcScanJourneyListSkeleton" +import { XcmTransferSkeleton } from "@/modules/xcm/transfer/XcmTransferSkeleton" + +export const XcmPageSkeleton = () => { + const { isConnected } = useAccount() + return ( + + + {isConnected && } + + ) +} diff --git a/apps/main/src/modules/xcm/history/XcJourneyCard.tsx b/apps/main/src/modules/xcm/history/XcJourneyCard.tsx index e9e43a3471..2648dc8776 100644 --- a/apps/main/src/modules/xcm/history/XcJourneyCard.tsx +++ b/apps/main/src/modules/xcm/history/XcJourneyCard.tsx @@ -13,11 +13,12 @@ import { Text, } from "@galacticcouncil/ui/components" import { getToken } from "@galacticcouncil/ui/utils" -import { shortenAccountAddress, xcscan } from "@galacticcouncil/utils" +import { xcscan } from "@galacticcouncil/utils" import type { XcJourney } from "@galacticcouncil/xc-scan" +import Big from "big.js" import { useTranslation } from "react-i18next" -import { isNumber } from "remeda" +import { AccountIdentity } from "@/components/AccountIdentity" import { ClaimButton } from "@/modules/xcm/history/components/ClaimButton" import { JourneyAssetLogo } from "@/modules/xcm/history/components/JourneyAssetLogo" import { JourneyChainLogo } from "@/modules/xcm/history/components/JourneyChainLogo" @@ -49,6 +50,8 @@ export const XcJourneyCard: React.FC = (journey) => { const isNotPending = !pendingCorrelationIds.includes(journey.correlationId) const isClaimable = isNotPending && isJourneyClaimable(journey) + const usdValue = Big(totalUsd || transferAsset?.usd || 0) + return ( @@ -111,9 +114,9 @@ export const XcJourneyCard: React.FC = (journey) => { symbol: transferAsset.symbol, })} - {isNumber(totalUsd) && totalUsd > 0 && ( + {usdValue.gt(0) && ( - {t("currency", { value: totalUsd })} + {t("currency", { value: usdValue })} )} @@ -123,16 +126,22 @@ export const XcJourneyCard: React.FC = (journey) => { {from && ( - - {t("from")}: {shortenAccountAddress(from)} - + + + {t("from")}: + + + )} {to && ( - - {t("to")}: {shortenAccountAddress(to)} - + + + {t("to")}: + + + )} diff --git a/apps/main/src/modules/xcm/history/XcScanHistoryTable.columns.tsx b/apps/main/src/modules/xcm/history/XcScanHistoryTable.columns.tsx index 93d6c41588..31ecf8c672 100644 --- a/apps/main/src/modules/xcm/history/XcScanHistoryTable.columns.tsx +++ b/apps/main/src/modules/xcm/history/XcScanHistoryTable.columns.tsx @@ -7,16 +7,14 @@ import { Text, } from "@galacticcouncil/ui/components" import { getToken } from "@galacticcouncil/ui/utils" -import { - shortenAccountAddress, - stringEquals, - xcscan, -} from "@galacticcouncil/utils" +import { stringEquals, xcscan } from "@galacticcouncil/utils" import type { XcJourney } from "@galacticcouncil/xc-scan" import { createColumnHelper } from "@tanstack/react-table" +import Big from "big.js" import { useMemo } from "react" import { useTranslation } from "react-i18next" +import { AccountIdentity } from "@/components/AccountIdentity" import { ClaimButton } from "@/modules/xcm/history/components/ClaimButton" import { JourneyAssetLogo } from "@/modules/xcm/history/components/JourneyAssetLogo" import { JourneyChainLogo } from "@/modules/xcm/history/components/JourneyChainLogo" @@ -56,9 +54,11 @@ export const useXcScanHistoryColumns = () => { return ( - - {shortenAccountAddress(from)} - + ) }, @@ -72,9 +72,11 @@ export const useXcScanHistoryColumns = () => { return ( - - {shortenAccountAddress(to)} - + ) }, @@ -88,6 +90,8 @@ export const useXcScanHistoryColumns = () => { if (!transferAsset) return null + const usdValue = Big(row.original.totalUsd || transferAsset?.usd || 0) + return ( @@ -103,9 +107,11 @@ export const useXcScanHistoryColumns = () => { }) : t("number", { value: transferAsset.amount })} - - {t("currency", { value: row.original.totalUsd })} - + {usdValue.gt(0) && ( + + {t("currency", { value: usdValue })} + + )} ) diff --git a/apps/main/src/modules/xcm/history/XcScanJourneyListSkeleton.tsx b/apps/main/src/modules/xcm/history/XcScanJourneyListSkeleton.tsx new file mode 100644 index 0000000000..eb75d44c47 --- /dev/null +++ b/apps/main/src/modules/xcm/history/XcScanJourneyListSkeleton.tsx @@ -0,0 +1,58 @@ +import { ArrowRight } from "@galacticcouncil/ui/assets/icons" +import { + Box, + Flex, + Icon, + LogoSkeleton, + Paper, + Separator, + Skeleton, + Stack, +} from "@galacticcouncil/ui/components" +import { getToken } from "@galacticcouncil/ui/utils" + +const JourneyCardSkeleton = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} + +export const XcScanJourneyListSkeleton = () => { + return ( + + {Array.from({ length: 4 }, (_, i) => ( + + ))} + + ) +} diff --git a/apps/main/src/modules/xcm/history/XcmHistoryPanel.tsx b/apps/main/src/modules/xcm/history/XcmHistoryPanel.tsx new file mode 100644 index 0000000000..4ba486f700 --- /dev/null +++ b/apps/main/src/modules/xcm/history/XcmHistoryPanel.tsx @@ -0,0 +1,69 @@ +import { + Chip, + Stack, + ToggleGroup, + ToggleGroupItem, +} from "@galacticcouncil/ui/components" +import type { XcJourney } from "@galacticcouncil/xc-scan" +import { useEffect, useState } from "react" +import { useTranslation } from "react-i18next" + +import { XcScanJourneyList } from "@/modules/xcm/history/XcScanJourneyList" + +enum TabView { + All = "all", + Claimable = "claimable", +} + +type XcmHistoryPanelProps = { + all: XcJourney[] + claimable: XcJourney[] +} + +const PAGE_SIZE = 4 + +export const XcmHistoryPanel: React.FC = ({ + all, + claimable, +}) => { + const { t } = useTranslation(["common"]) + const [filter, setFilter] = useState(TabView.All) + + const shouldRenderFilter = claimable.length > 0 + + useEffect(() => { + if (claimable.length === 0 && filter === TabView.Claimable) { + setFilter(TabView.All) + } + }, [claimable.length, filter]) + + return ( + + {shouldRenderFilter && ( + + type="single" + size="medium" + value={filter} + onValueChange={(value) => value && setFilter(value)} + > + {t("all")} + + {t("claimable")} {claimable.length} + + + )} + {filter === TabView.All && ( + + )} + {filter === TabView.Claimable && ( + + )} + + ) +} diff --git a/apps/main/src/modules/xcm/history/hooks/useClaimableTransactions.ts b/apps/main/src/modules/xcm/history/hooks/useClaimableTransactions.ts new file mode 100644 index 0000000000..d2a785c424 --- /dev/null +++ b/apps/main/src/modules/xcm/history/hooks/useClaimableTransactions.ts @@ -0,0 +1,17 @@ +import { useAccount } from "@galacticcouncil/web3-connect" +import { useMemo } from "react" + +import { usePendingClaimsStore } from "@/modules/xcm/history/hooks/usePendingClaimsStore" +import { useXcScan } from "@/modules/xcm/history/useXcScan" + +export const useClaimableTransactions = () => { + const { account } = useAccount() + const { data: claimable } = useXcScan(account?.address ?? "", { + claimableOnly: true, + }) + const { pendingCorrelationIds } = usePendingClaimsStore() + return useMemo(() => { + const pending = new Set(pendingCorrelationIds) + return claimable.filter(({ correlationId }) => !pending.has(correlationId)) + }, [claimable, pendingCorrelationIds]) +} diff --git a/apps/main/src/modules/xcm/history/useXcScan.ts b/apps/main/src/modules/xcm/history/useXcScan.ts index f68658baba..5a8aaadf1a 100644 --- a/apps/main/src/modules/xcm/history/useXcScan.ts +++ b/apps/main/src/modules/xcm/history/useXcScan.ts @@ -21,6 +21,7 @@ export const useXcScan = (address: string, options: XcScanOptions = {}) => { enabled: !!address, staleTime: Infinity, refetchOnWindowFocus: false, + initialDataUpdatedAt: 0, initialData: [], select: claimableOnly ? getClaimableJourneys : undefined, queryFn: () => [], diff --git a/apps/main/src/modules/xcm/history/utils/journey.ts b/apps/main/src/modules/xcm/history/utils/journey.ts index 935be8c7e5..2a359add63 100644 --- a/apps/main/src/modules/xcm/history/utils/journey.ts +++ b/apps/main/src/modules/xcm/history/utils/journey.ts @@ -92,5 +92,5 @@ export function getFormattedAddresses(journey: XcJourney) { : journey.fromFormatted || "" const to = isH160Address(journey.to) ? journey.to : journey.toFormatted || "" - return { from: from.toLowerCase(), to: to.toLowerCase() } + return { from, to } } diff --git a/apps/main/src/modules/xcm/transfer/XcmTransferSkeleton.tsx b/apps/main/src/modules/xcm/transfer/XcmTransferSkeleton.tsx index 54b088f579..85be4d7c3e 100644 --- a/apps/main/src/modules/xcm/transfer/XcmTransferSkeleton.tsx +++ b/apps/main/src/modules/xcm/transfer/XcmTransferSkeleton.tsx @@ -39,7 +39,13 @@ const XcmSectionSkeleton = () => { export const XcmTransferSkeleton = () => { const { t } = useTranslation("xcm") return ( - + diff --git a/apps/main/src/routes/cross-chain/index.tsx b/apps/main/src/routes/cross-chain/index.tsx index fa3993b879..0613e5f3af 100644 --- a/apps/main/src/routes/cross-chain/index.tsx +++ b/apps/main/src/routes/cross-chain/index.tsx @@ -1,11 +1,11 @@ import { createFileRoute } from "@tanstack/react-router" import { xcmQueryParamsSchema } from "@/modules/xcm/transfer/utils/query" -import { XcmTransferSkeleton } from "@/modules/xcm/transfer/XcmTransferSkeleton" import { XcmPage } from "@/modules/xcm/XcmPage" +import { XcmPageSkeleton } from "@/modules/xcm/XcmPageSkeleton" export const Route = createFileRoute("/cross-chain/")({ component: XcmPage, - pendingComponent: XcmTransferSkeleton, + pendingComponent: XcmPageSkeleton, validateSearch: xcmQueryParamsSchema, }) diff --git a/packages/ui/src/components/ExternalLink/ExternalLink.styled.ts b/packages/ui/src/components/ExternalLink/ExternalLink.styled.ts new file mode 100644 index 0000000000..114b227110 --- /dev/null +++ b/packages/ui/src/components/ExternalLink/ExternalLink.styled.ts @@ -0,0 +1,9 @@ +import { styled } from "@/utils" + +export const SLink = styled.a<{ underlined?: boolean }>` + color: inherit; + text-decoration: ${({ underlined }) => (underlined ? "underline" : "none")}; + &:hover { + text-decoration: underline; + } +` diff --git a/packages/ui/src/components/ExternalLink/ExternalLink.tsx b/packages/ui/src/components/ExternalLink/ExternalLink.tsx index 4317fe8fb8..2b6f71e5bc 100644 --- a/packages/ui/src/components/ExternalLink/ExternalLink.tsx +++ b/packages/ui/src/components/ExternalLink/ExternalLink.tsx @@ -1,11 +1,23 @@ import { AnchorHTMLAttributes, FC, Ref } from "react" +import { SLink } from "@/components/ExternalLink/ExternalLink.styled" + type ExternalLinkProps = AnchorHTMLAttributes & { + underlined?: boolean readonly ref?: Ref } -export const ExternalLink: FC = (props) => { +export const ExternalLink: FC = ({ + underlined = true, + ...props +}) => { return ( - + ) } diff --git a/packages/ui/src/components/Icon/Icon.tsx b/packages/ui/src/components/Icon/Icon.tsx index b7975cd18e..f5ef8fd8df 100644 --- a/packages/ui/src/components/Icon/Icon.tsx +++ b/packages/ui/src/components/Icon/Icon.tsx @@ -1,4 +1,5 @@ -import { Box, BoxProps } from "@/components/Box" +import { BoxProps } from "@/components/Box" +import { Flex } from "@/components/Flex" type IconProps = BoxProps & { component: React.ComponentType @@ -10,8 +11,12 @@ export const Icon: React.FC = ({ color = "currentColor", ...props }) => ( - = ({ {...props} > - + ) diff --git a/packages/ui/src/components/ToggleGroup/ToggleGroup.styled.ts b/packages/ui/src/components/ToggleGroup/ToggleGroup.styled.ts index cf054dea21..3b77c1fc4c 100644 --- a/packages/ui/src/components/ToggleGroup/ToggleGroup.styled.ts +++ b/packages/ui/src/components/ToggleGroup/ToggleGroup.styled.ts @@ -84,6 +84,10 @@ export const SToggleGroupItem = styled(ToggleGroupPrimitive.Item, { background-color: transparent; color: ${theme.icons.onSurface}; + &:hover { + background-color: ${theme.buttons.secondary.low.hover}; + } + &[data-state="on"] { background-color: ${theme.buttons.primary.medium.rest}; color: ${theme.buttons.primary.medium.onButton}; diff --git a/packages/web3-connect/src/components/account/AccountIdentity.tsx b/packages/web3-connect/src/components/account/AccountIdentity.tsx index 642496dcad..66421f33f2 100644 --- a/packages/web3-connect/src/components/account/AccountIdentity.tsx +++ b/packages/web3-connect/src/components/account/AccountIdentity.tsx @@ -1,10 +1,13 @@ import { hydration } from "@galacticcouncil/descriptors" -import { Text, TextProps } from "@galacticcouncil/ui/components" +import { ExternalLink, Text, TextProps } from "@galacticcouncil/ui/components" import { + HYDRATION_CHAIN_KEY, isSS58Address, safeConvertSS58toPublicKey, + shorten, shortenAccountAddress, stringEquals, + subscan, } from "@galacticcouncil/utils" import { useAddressStore } from "@galacticcouncil/web3-connect/src/components/address-book/AddressBook.store" import { useQuery } from "@tanstack/react-query" @@ -12,14 +15,18 @@ import { TypedApi } from "polkadot-api" import { getIdentityQuery } from "@/utils/identity" +const MAX_DISPLAY_NAME_LENGTH = 15 + export type AccountIdentityProps = TextProps & { papi: TypedApi address: string + withSubscanLink?: boolean } export const AccountSubstrateIdentity: React.FC = ({ papi, address, + withSubscanLink = true, ...props }) => { const addresses = useAddressStore((state) => state.addresses) @@ -31,23 +38,52 @@ export const AccountSubstrateIdentity: React.FC = ({ getIdentityQuery(papi, !addressBookName ? address : ""), ) - const displayName = - addressBookName || identity?.display || shortenAccountAddress(address) + const displayName = addressBookName + ? shorten(addressBookName, MAX_DISPLAY_NAME_LENGTH) + : identity?.display || shortenAccountAddress(address) - return {displayName} + return ( + + {withSubscanLink ? ( + + {displayName} + + ) : ( + displayName + )} + + ) } export const AccountAddressBookIdentity: React.FC< Omit -> = ({ address, ...props }) => { +> = ({ address, withSubscanLink = true, ...props }) => { const addresses = useAddressStore((state) => state.addresses) const addressBookName = addresses.find((a) => stringEquals(a.address, address), )?.name - const displayName = addressBookName || shortenAccountAddress(address) + const displayName = addressBookName + ? shorten(addressBookName, MAX_DISPLAY_NAME_LENGTH) + : shortenAccountAddress(address) - return {displayName} + return ( + + {withSubscanLink ? ( + + {displayName} + + ) : ( + displayName + )} + + ) } export const AccountIdentity: React.FC = ({