Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions packages/widget/src/domain/earn/stake.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Array as EArray, Option } from "effect";
import { exactDecimal, exactZero } from "../finance/exact";
import type { YieldId } from "../identity/identifiers";
import type { Network } from "../network/network";
import { equalTokens, type Token } from "../token/token";
import { equalTokens, isNativeToken, type Token } from "../token/token";
import type { EarnValidator, EarnYieldWithProvider } from "./models";
import type { ValidatorKey } from "./validator";
import { getYieldActionArg, isBittensorStaking } from "./yield";
Expand All @@ -13,8 +13,11 @@ export const stakeTokenSameAsGasToken = ({
yieldDto,
}: {
stakeToken: Token;
yieldDto: EarnYieldWithProvider;
}) => equalTokens(stakeToken, yieldDto.mechanics.gasFeeToken);
yieldDto: EarnYieldWithProvider | null;
}) =>
isNativeToken(stakeToken) ||
(yieldDto !== null &&
equalTokens(stakeToken, yieldDto.mechanics.gasFeeToken));

export const getMaxAmount = ({
availableAmount,
Expand Down
1 change: 1 addition & 0 deletions packages/widget/src/domain/token/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ export const tokenString = (token: TokenLike): TokenString => {

export const equalTokens = (a: TokenLike, b: TokenLike) =>
tokenString(a) === tokenString(b);
export const isNativeToken = (token: TokenLike) => token.address === undefined;
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useAtomMount, useAtomSet, useAtomValue } from "@effect/atom-react";
import { createContext, useContext } from "react";
import { Navigate, Outlet, useMatch, useParams } from "react-router";
import { ContentLoaderSquare } from "../../../shared/ui/primitives/content-loader";
import { LoadingSkeleton } from "../../../shared/ui/components/loading-skeleton";
import { YieldActionContinuationSessionRoute } from "../../classic-transaction-flow/views";
import { walletScopeAtom } from "../../wallet/index";
import type { YieldSummaryProvider } from "../../yield-summary/index";
Expand Down Expand Up @@ -83,7 +83,7 @@ const BoundActivityActionRoute = ({
const retry = useAtomSet(retryActivityActionRouteAtom(selectionKey));

if (result.status === "loading") {
return <ContentLoaderSquare heightPx={320} />;
return <LoadingSkeleton />;
}
if (result.status === "failed") {
return <ActivityDetailsFailure onRetry={() => retry(undefined)} />;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { useTranslation } from "react-i18next";
import { VirtualList } from "../../../../shared/ui/components/virtual-list";
import { Box } from "../../../../shared/ui/primitives/box";
import { Button } from "../../../../shared/ui/primitives/button";
import { ContentLoaderSquare } from "../../../../shared/ui/primitives/content-loader";
import { Text } from "../../../../shared/ui/primitives/typography/text";
import { FallbackContent } from "../../../widget-shell/views";
import type { ActivityActionItem } from "../../model/activity-action";
Expand All @@ -12,7 +11,10 @@ import type {
ActivityPagePagination,
ActivityPageView,
} from "../../state/page";
import { ActionListItem } from "./components/action-list-item";
import {
ActionListItem,
ActionListItemSkeleton,
} from "./components/action-list-item";
import { ActivityFilters } from "./components/activity-filters";
import { container } from "./style.css";

Expand All @@ -23,11 +25,10 @@ const ActivityPageSkeleton = () => (
aria-hidden="true"
data-rk="activity-page-skeleton"
display="flex"
gap="1"
flexDirection="column"
>
{[...Array(5).keys()].map((item) => (
<ContentLoaderSquare key={item} heightPx={60} />
<ActionListItemSkeleton key={item} />
))}
</Box>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { useTranslation } from "react-i18next";
import { Box } from "../../../../../../shared/ui/primitives/box";
import { ContentLoaderLine } from "../../../../../../shared/ui/primitives/content-loader";
import { ListItem } from "../../../../../../shared/ui/primitives/list/list-item";
import { Text } from "../../../../../../shared/ui/primitives/typography/text";
import type { ActivityActionItem } from "../../../../model/activity-action";
import type { ActivityStatusLabel } from "../../../../model/activity-action-list-item";
import { useActionListItem } from "../../hooks/use-action-list-item";
import { ActivityIcon } from "../activity-icon";
import { ActivityIcon, type ActivityIconType } from "../activity-icon";
import {
amountNeutral,
amountPositive,
Expand Down Expand Up @@ -33,9 +35,58 @@ export const ActionListItem = ({

if (!listItemView) return null;

const { providersDetails } = listItemView;

const firstProvider = providersDetails?.[0];
const providerLabel = firstProvider
? t("positions.via", {
providerName: firstProvider.name ?? firstProvider.address,
count: Math.max((providersDetails?.length ?? 0) - 1, 1),
})
: null;
return (
<ActionListItemPresentation
view={listItemView}
viaLabel={providerLabel}
isSelected={isSelected}
onSelect={() => onActionSelect(action)}
/>
);
};

export const ActionListItemSkeleton = () => <ActionListItemPresentation />;

type ActionListItemContent = {
readonly canOpenDetails: boolean;
readonly iconType: ActivityIconType;
readonly title: string;
readonly tokenSymbol: string | null;
readonly amount: string | null;
readonly amountSign: "" | "+" | "-";
readonly isPositive: boolean;
readonly timestampAbsolute: string;
readonly timestampRelative: string;
readonly badgeLabel: string | null;
readonly statusLabel: ActivityStatusLabel | null;
};

const ActionListItemPresentation = ({
view,
viaLabel,
isSelected = false,
onSelect,
}: {
readonly view?: ActionListItemContent;
readonly viaLabel?: string | null;
readonly isSelected?: boolean;
readonly onSelect?: () => void;
}) => {
const loading = !view;
const readyDataRk = isSelected
? "activity-list-item-selected"
: "activity-list-item";
const {
canOpenDetails,
providersDetails,
iconType,
title,
tokenSymbol,
Expand All @@ -46,25 +97,14 @@ export const ActionListItem = ({
timestampRelative,
badgeLabel,
statusLabel,
} = listItemView;

const firstProvider = providersDetails?.[0];
const providerLabel = firstProvider
? t("positions.via", {
providerName: firstProvider.name ?? firstProvider.address,
count: Math.max((providersDetails?.length ?? 0) - 1, 1),
})
: null;
const viaLabel = providerLabel;
} = view ?? {};

return (
<Box py="1" width="full">
<Box py="1" width="full" aria-hidden={loading || undefined}>
<ListItem
onClick={canOpenDetails ? () => onActionSelect(action) : undefined}
onClick={canOpenDetails ? onSelect : undefined}
className={listItem}
data-rk={
isSelected ? "activity-list-item-selected" : "activity-list-item"
}
data-rk={loading ? "activity-list-item-skeleton" : readyDataRk}
variant={{
active: isSelected ? "active" : "inactive",
hover: canOpenDetails ? "enabled" : "disabled",
Expand All @@ -88,9 +128,11 @@ export const ActionListItem = ({
<ActivityIcon type={iconType} />

<Box className={infoColumn}>
<Text className={titleText}>{title}</Text>
<Text className={titleText}>
{loading ? <ContentLoaderLine widthPx="14ch" /> : title}
</Text>

{badgeLabel || viaLabel ? (
{loading || badgeLabel || viaLabel ? (
<Box className={metaRow}>
{badgeLabel && statusLabel ? (
<Box
Expand All @@ -112,12 +154,16 @@ export const ActionListItem = ({
</Box>
) : null}

{viaLabel ? (
{loading || viaLabel ? (
<Text
className={viaText}
variant={{ type: "muted", weight: "normal" }}
>
{viaLabel}
{loading ? (
<ContentLoaderLine widthPx="10ch" />
) : (
viaLabel
)}
</Text>
) : null}
</Box>
Expand All @@ -132,23 +178,37 @@ export const ActionListItem = ({
gap="3"
flexShrink={0}
>
{amount ? (
{loading || amount ? (
<Text className={isPositive ? amountPositive : amountNeutral}>
{amountSign}
{tokenSymbol ? `${amount} ${tokenSymbol}` : amount}
{loading ? (
<ContentLoaderLine widthPx="8ch" />
) : (
<>
{amountSign}
{tokenSymbol ? `${amount} ${tokenSymbol}` : amount}
</>
)}
</Text>
) : null}

<Box className={timeColumn}>
<Text
variant={{ type: "muted", weight: "normal", size: "small" }}
>
{timestampAbsolute}
{loading ? (
<ContentLoaderLine widthPx="9ch" />
) : (
timestampAbsolute
)}
</Text>
<Text
variant={{ type: "muted", weight: "normal", size: "small" }}
>
{timestampRelative}
{loading ? (
<ContentLoaderLine widthPx="6ch" />
) : (
timestampRelative
)}
</Text>
</Box>
</Box>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,35 +1,41 @@
import { Match } from "effect";
import { Box } from "../../../../../../shared/ui/primitives/box";
import { ContentLoaderCircle } from "../../../../../../shared/ui/primitives/content-loader";
import { Arrow } from "../../../../../../shared/ui/primitives/icons/arrow";
import { GifIcon } from "../../../../../../shared/ui/primitives/icons/gift";
import { iconCircle } from "../activity-item.css";

export type ActivityIconType = "in" | "neutral" | "out" | "rewards";

export const ActivityIcon = ({ type }: { type: ActivityIconType }) => {
const icon = Match.value(type).pipe(
Match.when("rewards", () => <GifIcon />),
Match.when("out", () => <Arrow direction="down" />),
Match.when("in", () => <Arrow direction="up" />),
Match.when("neutral", () => (
<svg
aria-hidden="true"
data-rk="activity-icon-neutral"
height="16"
viewBox="0 0 16 16"
width="16"
>
<path
d="M3 8h10"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeWidth="1.5"
/>
</svg>
)),
Match.exhaustive
);
export const ActivityIcon = ({ type }: { type?: ActivityIconType }) => {
const icon =
type === undefined ? (
<ContentLoaderCircle />
) : (
Match.value(type).pipe(
Match.when("rewards", () => <GifIcon />),
Match.when("out", () => <Arrow direction="down" />),
Match.when("in", () => <Arrow direction="up" />),
Match.when("neutral", () => (
<svg
aria-hidden="true"
data-rk="activity-icon-neutral"
height="16"
viewBox="0 0 16 16"
width="16"
>
<path
d="M3 8h10"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeWidth="1.5"
/>
</svg>
)),
Match.exhaustive
)
);

return <Box className={iconCircle}>{icon}</Box>;
};
Loading
Loading