Skip to content

Commit

Permalink
feat: ✨ (llm): new accounts and assets lists portfolio
Browse files Browse the repository at this point in the history
  • Loading branch information
LucasWerey committed Dec 10, 2024
1 parent c23401a commit bca80ac
Show file tree
Hide file tree
Showing 7 changed files with 222 additions and 13 deletions.
3 changes: 2 additions & 1 deletion apps/ledger-live-mobile/src/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -2342,7 +2342,8 @@
"seeMarket": "See all"
},
"walletBalance": "Wallet balance",
"seelAllAssets": "See All Assets",
"seeAllAssets": "See all assets",
"seeAllAccounts": "See all accounts",
"marketPriceSection": {
"title": "{{currencyTicker}} market price",
"currencyPrice": "1 {{currencyTicker}} price",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { Flex } from "@ledgerhq/native-ui";
import { Pressable } from "react-native";
import AccountItem from "./components/AccountItem";
import globalSyncRefreshControl from "~/components/globalSyncRefreshControl";
import { withDiscreetMode } from "~/context/DiscreetModeContext";
import isEqual from "lodash/isEqual";

const ESTIMED_ITEM_SIZE = 150;

Expand Down Expand Up @@ -50,4 +52,4 @@ const AccountsListView: React.FC<Props> = props => {
return <View {...viewModel} />;
};

export default AccountsListView;
export default React.memo(withDiscreetMode(AccountsListView), isEqual);
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import AssetItem from "./components/AssetItem";
import { Asset } from "~/types/asset";
import BigNumber from "bignumber.js";
import globalSyncRefreshControl from "~/components/globalSyncRefreshControl";
import { withDiscreetMode } from "~/context/DiscreetModeContext";
import isEqual from "lodash/isEqual";

const ESTIMED_ITEM_SIZE = 150;

Expand Down Expand Up @@ -53,4 +55,4 @@ const AssetsListView: React.FC<Props> = props => {
return <View {...viewModel} />;
};

export default AssetsListView;
export default React.memo(withDiscreetMode(AssetsListView), isEqual);
77 changes: 68 additions & 9 deletions apps/ledger-live-mobile/src/screens/Portfolio/PortfolioAssets.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
import { Button, IconsLegacy } from "@ledgerhq/native-ui";
import { useNavigation } from "@react-navigation/native";
import React, { useCallback, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { useSelector } from "react-redux";
import { useStartProfiler } from "@shopify/react-native-performance";
import { GestureResponderEvent } from "react-native";
import Animated from "react-native-reanimated";
import { useNavigation } from "@react-navigation/native";
import { Button, IconsLegacy, Box, Flex } from "@ledgerhq/native-ui";
import { useDistribution } from "~/actions/general";
import { TrackScreen } from "~/analytics";
import { NavigatorName, ScreenName } from "~/const";
import { Box } from "@ledgerhq/native-ui";
import { blacklistedTokenIdsSelector, discreetModeSelector } from "~/reducers/settings";
import Assets from "./Assets";
import PortfolioQuickActionsBar from "./PortfolioQuickActionsBar";
import { useFeature } from "@ledgerhq/live-common/featureFlags/index";
import TabSelector from "./TabSelector";
import AccountsListView from "LLM/features/Accounts/components/AccountsListView";
import AssetsListView from "LLM/features/Assets/components/AssetsListView";
import AddAccountButton from "LLM/features/Accounts/components/AddAccountButton";
import useTabAnimation from "./useTabAnimation";

type Props = {
hideEmptyTokenAccount: boolean;
Expand Down Expand Up @@ -49,9 +54,36 @@ const PortfolioAssets = ({ hideEmptyTokenAccount, openAddModal }: Props) => {
[distribution, blacklistedTokenIdsSet],
);

const goToAssets = useCallback(
const { selectedTab, handleToggle, handleLayout, assetsAnimatedStyle, accountsAnimatedStyle } =
useTabAnimation("Assets");

const showAssets = selectedTab === "Assets";
const showAccounts = selectedTab === "Accounts";

const memoizedAssetsListView = useMemo(
() => <AssetsListView limitNumberOfAssets={maxAssetsToDisplay} />,
[],
);
const memoizedAccountsListView = useMemo(
() => <AccountsListView limitNumberOfAccounts={maxAssetsToDisplay} />,
[],
);

const onPressButton = useCallback(
(uiEvent: GestureResponderEvent) => {
startNavigationTTITimer({ source: ScreenName.Portfolio, uiEvent });
if (!showAssets && isAccountListUIEnabled) {
navigation.navigate(NavigatorName.Accounts, {
screen: ScreenName.AccountsList,
params: {
sourceScreenName: ScreenName.Portfolio,
showHeader: true,
canAddAccount: true,
isSyncEnabled: true,
},
});
return;
}
if (isAccountListUIEnabled) {
navigation.navigate(NavigatorName.Assets, {
screen: ScreenName.AssetsList,
Expand All @@ -67,20 +99,47 @@ const PortfolioAssets = ({ hideEmptyTokenAccount, openAddModal }: Props) => {
});
}
},
[startNavigationTTITimer, isAccountListUIEnabled, navigation],
[startNavigationTTITimer, showAssets, isAccountListUIEnabled, navigation],
);

const showAddAccountButton =
isAccountListUIEnabled && showAccounts && distribution.list.length >= maxAssetsToDisplay;

return (
<>
<TrackScreen
category="Wallet"
accountsLength={distribution.list && distribution.list.length}
discreet={discreetMode}
/>
<Box mb={24} mt={18}>
<Box my={24}>
<PortfolioQuickActionsBar />
</Box>
<Assets assets={assetsToDisplay} />
{isAccountListUIEnabled ? (
<>
<Box
height={40}
mb={16}
borderRadius={12}
border={1}
borderColor="opacityDefault.c10"
p={2}
>
<TabSelector labels={["Assets", "Accounts"]} onToggle={handleToggle} />
</Box>
<Flex flexDirection="row" overflow="hidden" onLayout={handleLayout} width="200%">
<Animated.View style={[{ flex: 1 }, assetsAnimatedStyle]}>
{memoizedAssetsListView}
</Animated.View>
<Animated.View style={[{ flex: 1 }, accountsAnimatedStyle]}>
{memoizedAccountsListView}
</Animated.View>
</Flex>
</>
) : (
<Assets assets={assetsToDisplay} />
)}
{showAddAccountButton ? <AddAccountButton /> : null}
{distribution.list.length < maxAssetsToDisplay ? (
<Button
type="shade"
Expand All @@ -94,8 +153,8 @@ const PortfolioAssets = ({ hideEmptyTokenAccount, openAddModal }: Props) => {
{t("account.emptyState.addAccountCta")}
</Button>
) : (
<Button type="shade" size="large" outline mt={6} onPress={goToAssets}>
{t("portfolio.seelAllAssets")}
<Button type="shade" size="large" outline onPress={onPressButton}>
{showAssets ? t("portfolio.seeAllAssets") : t("portfolio.seeAllAccounts")}
</Button>
)}
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ function ReadOnlyPortfolio({ navigation }: NavigationProps) {
<Box background={colors.background.main} px={6} mt={6} key="Assets">
<Assets assets={assetsToDisplay} />
<Button type="shade" size="large" outline mt={6} onPress={goToAssets}>
{t("portfolio.seelAllAssets")}
{t("portfolio.seeAllAssets")}
</Button>
</Box>,
...(!hasOrderedNano
Expand Down
76 changes: 76 additions & 0 deletions apps/ledger-live-mobile/src/screens/Portfolio/TabSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import React, { useState } from "react";
import { Pressable, LayoutChangeEvent } from "react-native";
import Animated, { useSharedValue, useAnimatedStyle, withTiming } from "react-native-reanimated";
import styled from "styled-components/native";
import { Flex, Text } from "@ledgerhq/native-ui";

const Container = styled(Flex)`
height: 100%;
justify-content: space-between;
flex-direction: row;
position: relative;
`;

const AnimatedBackground = styled(Animated.View)`
position: absolute;
height: 100%;
border-radius: 8px;
background-color: ${({ theme }) => theme.colors.opacityDefault.c05};
`;

const Tab = styled(Flex)`
flex: 1;
padding: 4px;
border-radius: 8px;
align-items: center;
justify-content: center;
`;

type TabSelectorProps = {
labels: string[];
onToggle: (value: string) => void;
};

const TabSelector = ({ labels, onToggle }: TabSelectorProps) => {
const translateX = useSharedValue(0);
const [containerWidth, setContainerWidth] = useState(0);

const handlePress = (value: string, index: number) => {
onToggle(value);
translateX.value = (containerWidth / labels.length) * index;
};

const handleLayout = (event: LayoutChangeEvent) => {
const { width } = event.nativeEvent.layout;
setContainerWidth(width);
};

const animatedStyle = useAnimatedStyle(() => {
return {
transform: [{ translateX: withTiming(translateX.value, { duration: 300 }) }],
width: containerWidth / labels.length,
};
});

return (
<Container onLayout={handleLayout}>
<AnimatedBackground style={animatedStyle} />
{labels.map((label, index) => (
<Pressable
hitSlop={16}
key={label}
onPress={() => handlePress(label, index)}
style={({ pressed }: { pressed: boolean }) => [{ opacity: pressed ? 0.5 : 1.0, flex: 1 }]}
>
<Tab>
<Text fontSize={14} fontWeight="semiBold" flexShrink={1} numberOfLines={1}>
{label}
</Text>
</Tab>
</Pressable>
))}
</Container>
);
};

export default TabSelector;
69 changes: 69 additions & 0 deletions apps/ledger-live-mobile/src/screens/Portfolio/useTabAnimation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// FILE: useTabAnimation.ts
import { useSharedValue, useAnimatedStyle, withTiming } from "react-native-reanimated";
import { useState, useEffect } from "react";
import { LayoutChangeEvent } from "react-native";

const useTabAnimation = (initialTab: string) => {
const [selectedTab, setSelectedTab] = useState(initialTab);
const [containerWidth, setContainerWidth] = useState(0);

const assetsTranslateX = useSharedValue(0);
const accountsTranslateX = useSharedValue(containerWidth);
const assetsOpacity = useSharedValue(1);
const accountsOpacity = useSharedValue(0);

const handleToggle = (value: string) => {
setSelectedTab(value);
};

const handleLayout = (event: LayoutChangeEvent) => {
const { width } = event.nativeEvent.layout;
setContainerWidth(width);
accountsTranslateX.value = width;
};

const assetsAnimatedStyle = useAnimatedStyle(() => {
return {
transform: [{ translateX: assetsTranslateX.value }],
opacity: assetsOpacity.value,
};
});

const accountsAnimatedStyle = useAnimatedStyle(() => {
return {
transform: [{ translateX: accountsTranslateX.value }],
opacity: accountsOpacity.value,
};
});

useEffect(() => {
if (selectedTab === "Assets") {
assetsTranslateX.value = withTiming(0, { duration: 300 });
assetsOpacity.value = withTiming(1, { duration: 300 });
accountsTranslateX.value = withTiming(containerWidth, { duration: 300 });
accountsOpacity.value = withTiming(0, { duration: 400 });
} else {
assetsTranslateX.value = withTiming(-containerWidth / 2, { duration: 300 });
assetsOpacity.value = withTiming(0, { duration: 400 });
accountsTranslateX.value = withTiming(-containerWidth / 2, { duration: 300 });
accountsOpacity.value = withTiming(1, { duration: 300 });
}
}, [
selectedTab,
assetsTranslateX,
accountsTranslateX,
containerWidth,
assetsOpacity,
accountsOpacity,
]);

return {
selectedTab,
handleToggle,
handleLayout,
assetsAnimatedStyle,
accountsAnimatedStyle,
};
};

export default useTabAnimation;

0 comments on commit bca80ac

Please sign in to comment.