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
50 changes: 50 additions & 0 deletions packages/widget/src/components/TokenList/PinTokenButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import PushPinIcon from '@mui/icons-material/PushPin'
import PushPinOutlinedIcon from '@mui/icons-material/PushPinOutlined'
import { IconButton } from '@mui/material'
import { usePinnedTokensStore } from '../../stores/pinnedTokens/PinnedTokensStore.js'

interface PinTokenButtonProps {
chainId: number
tokenAddress: string
}

export const PinTokenButton = ({
chainId,
tokenAddress,
}: PinTokenButtonProps) => {
const [pinnedTokens, pinToken, unpinToken] = usePinnedTokensStore((state) => [
state.pinnedTokens,
state.pinToken,
state.unpinToken,
])

const isPinned =
pinnedTokens[chainId]?.includes(tokenAddress.toLowerCase()) ?? false

const handleClick = (e: React.MouseEvent) => {
e.stopPropagation()
;(e.currentTarget as HTMLElement).blur()
if (isPinned) {
unpinToken(chainId, tokenAddress)
} else {
pinToken(chainId, tokenAddress)
}
}

const PinIcon = isPinned ? PushPinIcon : PushPinOutlinedIcon

return (
<IconButton
sx={{ width: 20, height: 20 }}
size="small"
onClick={handleClick}
>
<PinIcon
sx={{
fontSize: 12,
color: isPinned ? 'text.primary' : 'text.secondary',
}}
/>
</IconButton>
)
}
2 changes: 2 additions & 0 deletions packages/widget/src/components/TokenList/TokenList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export const TokenList: FC<TokenListProps> = memo(({ formType, headerRef }) => {
const {
tokens,
withCategories,
withPinnedTokens,
isTokensLoading,
isBalanceLoading,
isSearchLoading,
Expand Down Expand Up @@ -77,6 +78,7 @@ export const TokenList: FC<TokenListProps> = memo(({ formType, headerRef }) => {
isLoading={isTokensLoading || isSearchLoading}
isBalanceLoading={isBalanceLoading}
showCategories={showCategories}
showPinnedTokens={withPinnedTokens}
onClick={handleTokenClick}
selectedTokenAddress={selectedTokenAddress}
isAllNetworks={isAllNetworks}
Expand Down
31 changes: 20 additions & 11 deletions packages/widget/src/components/TokenList/TokenListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { formatTokenAmount, formatTokenPrice } from '../../utils/format.js'
import { shortenAddress } from '../../utils/wallet.js'
import { TokenAvatar } from '../Avatar/TokenAvatar.js'
import { ListItemButton } from '../ListItem/ListItemButton.js'
import { PinTokenButton } from './PinTokenButton.js'
import { IconButton, ListItem } from './TokenList.style.js'
import type {
TokenListItemAvatarProps,
Expand Down Expand Up @@ -226,17 +227,17 @@ const TokenListItemButton: React.FC<TokenListItemButtonProps> = memo(
appear={false}
mountOnEnter
>
<Box
sx={{
display: 'flex',
}}
>
<Box>
<OpenTokenDetailsButton
tokenAddress={token.address}
withoutContractAddress={withoutContractAddress}
chainId={token.chainId}
onClick={onShowTokenDetails}
/>
<PinTokenButton
chainId={token.chainId}
tokenAddress={token.address}
/>
</Box>
</Slide>
</Box>
Expand Down Expand Up @@ -279,6 +280,8 @@ const TokenListItemButton: React.FC<TokenListItemButtonProps> = memo(
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 0.5,
}}
>
<Box
Expand All @@ -290,12 +293,18 @@ const TokenListItemButton: React.FC<TokenListItemButtonProps> = memo(
>
{shortenAddress(token.address)}
</Box>
<OpenTokenDetailsButton
tokenAddress={token.address}
withoutContractAddress={withoutContractAddress}
chainId={token.chainId}
onClick={onShowTokenDetails}
/>
<Box>
<OpenTokenDetailsButton
tokenAddress={token.address}
withoutContractAddress={withoutContractAddress}
chainId={token.chainId}
onClick={onShowTokenDetails}
/>
<PinTokenButton
chainId={token.chainId}
tokenAddress={token.address}
/>
</Box>
</Box>
</Slide>
</Box>
Expand Down
133 changes: 80 additions & 53 deletions packages/widget/src/components/TokenList/VirtualizedTokenList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const VirtualizedTokenList: FC<VirtualizedTokenListProps> = ({
isLoading,
isBalanceLoading,
showCategories,
showPinnedTokens,
onClick,
isAllNetworks,
}) => {
Expand Down Expand Up @@ -62,29 +63,33 @@ export const VirtualizedTokenList: FC<VirtualizedTokenListProps> = ({
const estimateSize = useCallback(
(index: number) => {
const currentToken = tokens[index]

// Base size for TokenListItem
const previousToken = tokens[index - 1]
let size = tokenItemHeight

// Early return if categories are not shown
// Pinned tokens (always shown, even in all networks mode)
if (currentToken.pinned && index === 0) {
size += 24
}
if (previousToken?.pinned && !currentToken.pinned) {
size += 32
}

if (!showCategories) {
return size
}

const previousToken = tokens[index - 1]

// Adjust size for the first featured token
if (currentToken.featured && index === 0) {
if (currentToken.featured && !currentToken.pinned && index === 0) {
size += 24
}

// Adjust size based on changes between the current and previous tokens
const isCategoryChanged =
(previousToken?.amount && !currentToken.amount) ||
(previousToken?.featured && !currentToken.featured) ||
(previousToken?.popular && !currentToken.popular)

if (isCategoryChanged) {
// Category transition (excluding pinned tokens)
const isNotPinned = !currentToken.pinned && !previousToken?.pinned
if (
isNotPinned &&
((previousToken?.amount && !currentToken.amount) ||
(previousToken?.featured && !currentToken.featured) ||
(previousToken?.popular && !currentToken.popular))
) {
size += 32
}

Expand All @@ -93,7 +98,6 @@ export const VirtualizedTokenList: FC<VirtualizedTokenListProps> = ({
[tokens, showCategories]
)

// Chunk the tokens for infinite loading simulation
const virtualizerConfig = useMemo(
() => ({
count: tokens.length,
Expand Down Expand Up @@ -135,47 +139,64 @@ export const VirtualizedTokenList: FC<VirtualizedTokenListProps> = ({
{getVirtualItems().map((item) => {
const currentToken = tokens[item.index]
const previousToken: TokenAmount | undefined = tokens[item.index - 1]

const chain = chainsSet?.get(currentToken.chainId)

const isFirstFeaturedToken = currentToken.featured && item.index === 0

const isTransitionFromFeaturedTokens =
previousToken?.featured && !currentToken.featured
const isNotPinned = !currentToken.pinned
const isFirstPinnedToken = currentToken.pinned && item.index === 0
const isTransitionFromPinned = previousToken?.pinned && isNotPinned

// Category transitions (excluding pinned)
const isTransitionFromFeatured =
previousToken?.featured && !currentToken.featured && isNotPinned
const isTransitionFromMyTokens =
previousToken?.amount && !currentToken.amount

const isTransitionToMyTokens =
isTransitionFromFeaturedTokens && currentToken.amount

const isTransitionToPopularTokens =
(isTransitionFromFeaturedTokens || isTransitionFromMyTokens) &&
currentToken.popular

const shouldShowAllTokensCategory =
isTransitionFromMyTokens ||
isTransitionFromFeaturedTokens ||
(previousToken?.popular && !currentToken.popular)

const startAdornmentLabel =
!isAllNetworks && showCategories
? (() => {
if (isFirstFeaturedToken) {
return t('main.featuredTokens')
}
if (isTransitionToMyTokens) {
return t('main.myTokens')
}
if (isTransitionToPopularTokens) {
return t('main.popularTokens')
}
if (shouldShowAllTokensCategory) {
return t('main.allTokens')
}
return null
})()
: null
previousToken?.amount && !currentToken.amount && isNotPinned
const isTransitionFromPopular =
previousToken?.popular && !currentToken.popular && isNotPinned

// Determine which category label to show
const startAdornmentLabel = (() => {
if (showPinnedTokens && isFirstPinnedToken) {
return t('main.pinnedTokens')
}
if (showPinnedTokens && !showCategories && isTransitionFromPinned) {
return t('main.allTokens')
}
if (!showCategories) {
return null
}

if (
(isTransitionFromPinned && currentToken.featured) ||
(currentToken.featured && isNotPinned && item.index === 0)
) {
return t('main.featuredTokens')
}
if (
(isTransitionFromFeatured || isTransitionFromPinned) &&
currentToken.amount &&
isNotPinned
) {
return t('main.myTokens')
}
if (
(isTransitionFromFeatured ||
isTransitionFromMyTokens ||
isTransitionFromPinned) &&
currentToken.popular &&
isNotPinned
) {
return t('main.popularTokens')
}
if (
isTransitionFromMyTokens ||
isTransitionFromFeatured ||
isTransitionFromPinned ||
isTransitionFromPopular
) {
return t('main.allTokens')
}
return null
})()

const isSelected =
selectedTokenAddress === currentToken.address &&
Expand All @@ -200,7 +221,13 @@ export const VirtualizedTokenList: FC<VirtualizedTokenListProps> = ({
fontWeight: 600,
lineHeight: '16px',
px: 1.5,
pt: isFirstFeaturedToken ? 0 : 1,
pt:
isFirstPinnedToken ||
(currentToken.featured &&
isNotPinned &&
item.index === 0)
? 0
: 1,
pb: 1,
}}
>
Expand Down
1 change: 1 addition & 0 deletions packages/widget/src/components/TokenList/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface VirtualizedTokenListProps {
isBalanceLoading: boolean
chainId?: number
showCategories?: boolean
showPinnedTokens?: boolean
onClick(tokenAddress: string, chainId?: number): void
selectedTokenAddress?: string
isAllNetworks: boolean
Expand Down
Loading