Skip to content
Draft
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@
"styled-components": "^6.0.7",
"tronweb": "6.1.0",
"use-long-press": "^3.3.0",
"use-pull-to-refresh": "^2.4.1",
"uuid": "^9.0.0",
"vaul": "^1.1.2",
"viem": "2.40.3",
Expand Down
5 changes: 5 additions & 0 deletions src/assets/translations/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -2639,5 +2639,10 @@
"description": "Your reward of %{amountAndSymbol} is complete."
}
}
},
"pullToRefresh": {
"pullToRefresh": "Pull to refresh",
"releaseToRefresh": "Release to refresh",
"refreshing": "Refreshing..."
}
}
58 changes: 55 additions & 3 deletions src/components/AssetAccounts/AssetAccounts.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import { Card, CardBody, CardHeader, Grid, Heading, Stack } from '@chakra-ui/react'
import { Card, CardBody, CardHeader, Grid, Heading, Stack, useMediaQuery } from '@chakra-ui/react'
import type { AccountId, AssetId } from '@shapeshiftoss/caip'
import { useIsFetching } from '@tanstack/react-query'
import { useCallback } from 'react'
import { useTranslate } from 'react-polyglot'

import { AssetAccountRow } from './AssetAccountRow'

import { PullToRefreshList } from '@/components/PullToRefresh/PullToRefreshList'
import { Text } from '@/components/Text'
import { useFeatureFlag } from '@/hooks/useFeatureFlag/useFeatureFlag'
import { portfolioApi } from '@/state/slices/portfolioSlice/portfolioSlice'
import { selectIsAnyPortfolioGetAccountLoading } from '@/state/slices/portfolioSlice/selectors'
import { selectAccountIdsByAssetIdAboveBalanceThreshold } from '@/state/slices/selectors'
import { useAppSelector } from '@/state/store'
import { txHistoryApi } from '@/state/slices/txHistorySlice/txHistorySlice'
import { useAppDispatch, useAppSelector } from '@/state/store'
import { breakpoints } from '@/theme/theme'

type AssetAccountsProps = {
assetId: AssetId
Expand All @@ -24,11 +32,45 @@

export const AssetAccounts = ({ assetId, accountId }: AssetAccountsProps) => {
const translate = useTranslate()
const [isMobile] = useMediaQuery(`(max-width: ${breakpoints['md']})`, { ssr: false })
const dispatch = useAppDispatch()
const isLazyTxHistoryEnabled = useFeatureFlag('LazyTxHistory')

const getAccountFetching = useIsFetching({ queryKey: ['getAccount'] })
const portalsAccountFetching = useIsFetching({ queryKey: ['portalsAccount'] })
const portalsPlatformsFetching = useIsFetching({ queryKey: ['portalsPlatforms'] })
const isPortfolioLoading = useAppSelector(selectIsAnyPortfolioGetAccountLoading)

const isRefreshing =
getAccountFetching > 0 ||
portalsAccountFetching > 0 ||
portalsPlatformsFetching > 0 ||
isPortfolioLoading
Comment on lines +44 to +48
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Memoize the derived isRefreshing value.

The isRefreshing value is computed from multiple state sources and recalculates on every render. Per coding guidelines, derived values should be wrapped in useMemo.

Apply this diff:

-  const isRefreshing =
-    getAccountFetching > 0 ||
-    portalsAccountFetching > 0 ||
-    portalsPlatformsFetching > 0 ||
-    isPortfolioLoading
+  const isRefreshing = useMemo(
+    () =>
+      getAccountFetching > 0 ||
+      portalsAccountFetching > 0 ||
+      portalsPlatformsFetching > 0 ||
+      isPortfolioLoading,
+    [getAccountFetching, portalsAccountFetching, portalsPlatformsFetching, isPortfolioLoading],
+  )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const isRefreshing =
getAccountFetching > 0 ||
portalsAccountFetching > 0 ||
portalsPlatformsFetching > 0 ||
isPortfolioLoading
const isRefreshing = useMemo(
() =>
getAccountFetching > 0 ||
portalsAccountFetching > 0 ||
portalsPlatformsFetching > 0 ||
isPortfolioLoading,
[getAccountFetching, portalsAccountFetching, portalsPlatformsFetching, isPortfolioLoading],
)
🤖 Prompt for AI Agents
In src/components/AssetAccounts/AssetAccounts.tsx around lines 44 to 48, the
derived isRefreshing boolean is computed on every render; wrap that computation
in React.useMemo to memoize it. Import useMemo from React if not already
imported, replace the direct assignment with useMemo(() => getAccountFetching >
0 || portalsAccountFetching > 0 || portalsPlatformsFetching > 0 ||
isPortfolioLoading, [getAccountFetching, portalsAccountFetching,
portalsPlatformsFetching, isPortfolioLoading]) so the value only recalculates
when those four dependencies change.


const accountIds = useAppSelector(state =>
selectAccountIdsByAssetIdAboveBalanceThreshold(state, { assetId }),
)

const handleRefresh = useCallback(async () => {

Check failure on line 54 in src/components/AssetAccounts/AssetAccounts.tsx

View workflow job for this annotation

GitHub Actions / Call / Static

Async arrow function has no 'await' expression
dispatch(portfolioApi.util.resetApiState())
dispatch(txHistoryApi.util.resetApiState())

const { getAllTxHistory } = txHistoryApi.endpoints

accountIds.forEach(accountId => {
dispatch(portfolioApi.endpoints.getAccount.initiate({ accountId, upsertOnFetch: true }))
})

if (isLazyTxHistoryEnabled) return

accountIds.forEach(requestedAccountId => {
dispatch(getAllTxHistory.initiate(requestedAccountId))
})
}, [dispatch, accountIds, isLazyTxHistoryEnabled])

if ((accountIds && accountIds.length === 0) || accountId) return null
return (

const content = (
<Card>
<CardHeader>
<Heading as='h5'>{translate('assets.assetDetails.assetAccounts.assetAllocation')}</Heading>
Expand Down Expand Up @@ -74,4 +116,14 @@
</CardBody>
</Card>
)

if (!isMobile) {
return content
}

return (
<PullToRefreshList onRefresh={handleRefresh} isRefreshing={isRefreshing}>
{content}
</PullToRefreshList>
)
}
124 changes: 124 additions & 0 deletions src/components/PullToRefresh/PullToRefreshList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import {

Check failure on line 1 in src/components/PullToRefresh/PullToRefreshList.tsx

View workflow job for this annotation

GitHub Actions / Call / Static

Replace `⏎··Box,⏎··Flex,⏎··Progress,⏎··Spinner,⏎··Text,⏎··useColorModeValue,⏎` with `·Box,·Flex,·Progress,·Spinner,·Text,·useColorModeValue·`
Box,
Flex,
Progress,
Spinner,
Text,
useColorModeValue,
} from '@chakra-ui/react'
import type { ReactNode } from 'react'
import { useMemo } from 'react'
import { FiRefreshCw } from 'react-icons/fi'
import { useTranslate } from 'react-polyglot'
import { usePullToRefresh } from 'use-pull-to-refresh'

type PullToRefreshListProps = {
children: ReactNode
onRefresh: () => void | Promise<void>
isRefreshing?: boolean
}

export const PullToRefreshList: React.FC<PullToRefreshListProps> = ({
children,
onRefresh,
isRefreshing = false,
}) => {
const translate = useTranslate()
const progressColor = useColorModeValue('blue.500', 'blue.400')
const textColor = useColorModeValue('gray.700', 'gray.200')

const { isRefreshing: isPulling, pullPosition } = usePullToRefresh({

Check failure on line 30 in src/components/PullToRefresh/PullToRefreshList.tsx

View workflow job for this annotation

GitHub Actions / Call / Static

'isPulling' is assigned a value but never used. Allowed unused vars must match /^_/u
onRefresh,
maximumPullLength: 150,
refreshThreshold: 100,
})

const pullProgress = useMemo(() => {
return Math.min(pullPosition / 100, 1)
}, [pullPosition])

const indicatorOpacity = useMemo(() => {
return Math.min(pullPosition / 40, 1)
}, [pullPosition])

const displayText = useMemo(() => {
if (isRefreshing) return translate('pullToRefresh.refreshing')
if (pullProgress >= 1) return translate('pullToRefresh.releaseToRefresh')
if (pullProgress > 0.2) return translate('pullToRefresh.pullToRefresh')
return ''
}, [isRefreshing, pullProgress, translate])

const iconRotation = useMemo(() => {
return isRefreshing ? 0 : pullProgress * 360
}, [isRefreshing, pullProgress])

const showIndicator = pullPosition > 0 || isRefreshing

return (
<Box position='relative' height='100%' overflow='auto'>
{/* Pull indicator */}
{showIndicator && (
<Box
position='absolute'
top={0}
left={0}
right={0}
height='90px'
display='flex'
alignItems='center'
justifyContent='center'
pointerEvents='none'
opacity={indicatorOpacity}
transform={`translateY(${Math.min(pullPosition - 90, 0)}px)`}
transition='opacity 0.2s ease-out'
zIndex={10}
>
<Flex direction='column' align='center' justify='center' gap={2}>
<Box>
{isRefreshing ? (
<Spinner size='sm' color={progressColor} thickness='2px' speed='0.8s' />
) : (
<Box
as={FiRefreshCw}
fontSize='24px'
color={progressColor}
transform={`rotate(${iconRotation}deg)`}
transition='transform 0.1s ease-out'
/>
)}
</Box>

<Box

Check failure on line 91 in src/components/PullToRefresh/PullToRefreshList.tsx

View workflow job for this annotation

GitHub Actions / Call / Static

Replace `⏎··············width='100%'⏎··············maxWidth='180px'⏎··············height='3px'⏎··············bg='whiteAlpha.200'⏎··············borderRadius='full'⏎············` with `·width='100%'·maxWidth='180px'·height='3px'·bg='whiteAlpha.200'·borderRadius='full'`
width='100%'
maxWidth='180px'
height='3px'
bg='whiteAlpha.200'
borderRadius='full'
>
{!isRefreshing && (
<Box
height='100%'
bg={progressColor}
borderRadius='full'
width={`${pullProgress * 100}%`}
transition='width 0.1s ease-out'
boxShadow={pullProgress >= 1 ? `0 0 8px ${progressColor}` : 'none'}
/>
)}
{isRefreshing && <Progress isIndeterminate={true} size='xs' colorScheme='blue' />}
</Box>

{displayText && (
<Text fontSize='xs' fontWeight='medium' color={textColor}>
{displayText}
</Text>
)}
</Flex>
</Box>
)}

{/* Content with dynamic padding to reveal indicator */}
<Box paddingTop={showIndicator ? `${Math.min(pullPosition, 90)}px` : 0}>{children}</Box>
</Box>
)
}
Loading