Commit 66a38c8e authored by lynn's avatar lynn Committed by GitHub

fix: add infinite scroll to view my nfts filter panel (#5216)

* infinite_scroll_working

* fix

* remove extra

* add infinite loading

* working

* remove null

* filter button text fixes

* respond to charlie plus mobile tooltip fixes

* make filter bar sticky

* respond to charlie
parent f79d2f82
...@@ -282,7 +282,7 @@ const Bag = () => { ...@@ -282,7 +282,7 @@ const Bag = () => {
return ( return (
<Portal> <Portal>
<Column zIndex={isMobile || isOpen ? 'modal' : '3'} className={styles.bagContainer}> <Column zIndex={isMobile || isOpen ? 'modalOverTooltip' : '3'} className={styles.bagContainer}>
{!(isProfilePage && profilePageState === ProfilePageStateType.LISTING) ? ( {!(isProfilePage && profilePageState === ProfilePageStateType.LISTING) ? (
<> <>
<BagHeader <BagHeader
......
import { ScrollBarStyles } from 'components/Common' import { ScrollBarStyles } from 'components/Common'
import { LoadingBubble } from 'components/Tokens/loading'
import { AnimatedBox, Box } from 'nft/components/Box' import { AnimatedBox, Box } from 'nft/components/Box'
import { Column, Row } from 'nft/components/Flex' import { Column, Row } from 'nft/components/Flex'
import { XMarkIcon } from 'nft/components/icons' import { XMarkIcon } from 'nft/components/icons'
import { Checkbox } from 'nft/components/layout/Checkbox' import { Checkbox } from 'nft/components/layout/Checkbox'
import { checkbox } from 'nft/components/layout/Checkbox.css'
import { Input } from 'nft/components/layout/Input' import { Input } from 'nft/components/layout/Input'
import { subhead } from 'nft/css/common.css' import { subhead } from 'nft/css/common.css'
import { themeVars } from 'nft/css/sprinkles.css' import { themeVars } from 'nft/css/sprinkles.css'
import { useFiltersExpanded, useIsMobile, useWalletCollections } from 'nft/hooks' import { useFiltersExpanded, useIsMobile, useWalletCollections } from 'nft/hooks'
import { WalletCollection } from 'nft/types' import { WalletCollection } from 'nft/types'
import { Dispatch, FormEvent, SetStateAction, useCallback, useEffect, useReducer, useState } from 'react' import { CSSProperties, Dispatch, FormEvent, SetStateAction, useCallback, useEffect, useReducer, useState } from 'react'
import { easings, useSpring } from 'react-spring' import { easings, useSpring } from 'react-spring'
import AutoSizer from 'react-virtualized-auto-sizer'
import { FixedSizeList, ListOnItemsRenderedProps } from 'react-window'
import InfiniteLoader from 'react-window-infinite-loader'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
import { TRANSITION_DURATIONS } from 'theme/styles' import { TRANSITION_DURATIONS } from 'theme/styles'
import * as styles from './ProfilePage.css' import * as styles from './ProfilePage.css'
const ItemsContainer = styled.div` const COLLECTION_ROW_HEIGHT = 44
const ItemsContainer = styled(Column)`
${ScrollBarStyles} ${ScrollBarStyles}
overflow-y: auto; height: 100vh;
`
const LongLoadingBubble = styled(LoadingBubble)`
min-height: 15px;
width: 75%;
` `
export const FilterSidebar = () => { const SmallLoadingBubble = styled(LoadingBubble)`
height: 20px;
width: 20px;
margin-right: 8px;
`
const LoadingCollectionItem = ({ style }: { style?: CSSProperties }) => {
return (
<Row display="flex" justifyContent="space-between" style={style} paddingLeft="12" paddingRight="16">
<Row display="flex" flex="1">
<SmallLoadingBubble />
<LongLoadingBubble />
</Row>
<Box as="span" borderColor="backgroundOutline" className={checkbox} aria-hidden="true" />
</Row>
)
}
interface CollectionFilterRowProps {
index: number
style: CSSProperties
}
interface FilterSidebarProps {
fetchNextPage: () => void
hasNextPage?: boolean
isFetchingNextPage: boolean
walletCollections: WalletCollection[]
}
export const FilterSidebar = ({
fetchNextPage,
hasNextPage,
isFetchingNextPage,
walletCollections,
}: FilterSidebarProps) => {
const collectionFilters = useWalletCollections((state) => state.collectionFilters) const collectionFilters = useWalletCollections((state) => state.collectionFilters)
const setCollectionFilters = useWalletCollections((state) => state.setCollectionFilters) const setCollectionFilters = useWalletCollections((state) => state.setCollectionFilters)
const walletCollections = useWalletCollections((state) => state.walletCollections)
const [isFiltersExpanded, setFiltersExpanded] = useFiltersExpanded() const [isFiltersExpanded, setFiltersExpanded] = useFiltersExpanded()
const isMobile = useIsMobile() const isMobile = useIsMobile()
...@@ -38,8 +84,8 @@ export const FilterSidebar = () => { ...@@ -38,8 +84,8 @@ export const FilterSidebar = () => {
return ( return (
// @ts-ignore // @ts-ignore
<AnimatedBox <AnimatedBox
position={{ sm: 'fixed', md: 'sticky' }} position="sticky"
top={{ sm: '40', md: 'unset' }} top="72"
left={{ sm: '0', md: 'unset' }} left={{ sm: '0', md: 'unset' }}
width={{ sm: 'full', md: '332', lg: '332' }} width={{ sm: 'full', md: '332', lg: '332' }}
height={{ sm: 'full', md: 'auto' }} height={{ sm: 'full', md: 'auto' }}
...@@ -70,6 +116,9 @@ export const FilterSidebar = () => { ...@@ -70,6 +116,9 @@ export const FilterSidebar = () => {
collections={walletCollections} collections={walletCollections}
collectionFilters={collectionFilters} collectionFilters={collectionFilters}
setCollectionFilters={setCollectionFilters} setCollectionFilters={setCollectionFilters}
fetchNextPage={fetchNextPage}
hasNextPage={hasNextPage}
isFetchingNextPage={isFetchingNextPage}
/> />
</Box> </Box>
</AnimatedBox> </AnimatedBox>
...@@ -80,10 +129,16 @@ const CollectionSelect = ({ ...@@ -80,10 +129,16 @@ const CollectionSelect = ({
collections, collections,
collectionFilters, collectionFilters,
setCollectionFilters, setCollectionFilters,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
}: { }: {
collections: WalletCollection[] collections: WalletCollection[]
collectionFilters: Array<string> collectionFilters: Array<string>
setCollectionFilters: (address: string) => void setCollectionFilters: (address: string) => void
fetchNextPage: () => void
hasNextPage?: boolean
isFetchingNextPage: boolean
}) => { }) => {
const [collectionSearchText, setCollectionSearchText] = useState('') const [collectionSearchText, setCollectionSearchText] = useState('')
const [displayCollections, setDisplayCollections] = useState(collections) const [displayCollections, setDisplayCollections] = useState(collections)
...@@ -99,6 +154,44 @@ const CollectionSelect = ({ ...@@ -99,6 +154,44 @@ const CollectionSelect = ({
} }
}, [collectionSearchText, collections]) }, [collectionSearchText, collections])
const itemKey = useCallback((index: number, data: WalletCollection[]) => {
if (!data) return index
const collection = data[index]
return `${collection.address}_${index}`
}, [])
// If there are more items to be loaded then add an extra row to hold a loading indicator.
const itemCount = hasNextPage ? displayCollections.length + 1 : displayCollections.length
// Only load 1 page of items at a time.
// Pass an empty callback to InfiniteLoader in case it asks us to load more than once.
const loadMoreItems = isFetchingNextPage ? () => null : fetchNextPage
// Every row is loaded except for our loading indicator row.
const isItemLoaded = useCallback(
(index: number) => !hasNextPage || index < displayCollections.length,
[displayCollections.length, hasNextPage]
)
const CollectionFilterRow = useCallback(
({ index, style }: CollectionFilterRowProps) => {
const collection = !!displayCollections && displayCollections[index]
if (!collection || isFetchingNextPage) {
return <LoadingCollectionItem style={style} key={index} />
}
return (
<CollectionItem
style={style}
key={itemKey(index, displayCollections)}
collection={displayCollections[index]}
collectionFilters={collectionFilters}
setCollectionFilters={setCollectionFilters}
/>
)
},
[displayCollections, isFetchingNextPage, itemKey, collectionFilters, setCollectionFilters]
)
return ( return (
<> <>
<Box className={subhead} marginTop="12" marginBottom="16" width="276"> <Box className={subhead} marginTop="12" marginBottom="16" width="276">
...@@ -111,16 +204,31 @@ const CollectionSelect = ({ ...@@ -111,16 +204,31 @@ const CollectionSelect = ({
setCollectionSearchText={setCollectionSearchText} setCollectionSearchText={setCollectionSearchText}
/> />
<ItemsContainer> <ItemsContainer>
<Box paddingBottom="8" style={{ scrollbarWidth: 'none' }}> <AutoSizer disableWidth>
{displayCollections?.map((collection) => ( {({ height }) => (
<CollectionItem <InfiniteLoader isItemLoaded={isItemLoaded} itemCount={itemCount} loadMoreItems={loadMoreItems}>
key={collection.address} {({
collection={collection} onItemsRendered,
collectionFilters={collectionFilters} ref,
setCollectionFilters={setCollectionFilters} }: {
/> onItemsRendered: (props: ListOnItemsRenderedProps) => any
))} ref: any
</Box> }) => (
<FixedSizeList
height={height}
width="100%"
itemCount={itemCount}
itemSize={COLLECTION_ROW_HEIGHT}
onItemsRendered={onItemsRendered}
itemKey={itemKey}
ref={ref}
>
{CollectionFilterRow}
</FixedSizeList>
)}
</InfiniteLoader>
)}
</AutoSizer>
</ItemsContainer> </ItemsContainer>
</Column> </Column>
</Box> </Box>
...@@ -153,10 +261,12 @@ const CollectionItem = ({ ...@@ -153,10 +261,12 @@ const CollectionItem = ({
collection, collection,
collectionFilters, collectionFilters,
setCollectionFilters, setCollectionFilters,
style,
}: { }: {
collection: WalletCollection collection: WalletCollection
collectionFilters: Array<string> collectionFilters: Array<string>
setCollectionFilters: (address: string) => void setCollectionFilters: (address: string) => void
style?: CSSProperties
}) => { }) => {
const [isCheckboxSelected, setCheckboxSelected] = useState(false) const [isCheckboxSelected, setCheckboxSelected] = useState(false)
const [hovered, toggleHovered] = useReducer((state) => { const [hovered, toggleHovered] = useReducer((state) => {
...@@ -187,8 +297,9 @@ const CollectionItem = ({ ...@@ -187,8 +297,9 @@ const CollectionItem = ({
style={{ style={{
paddingBottom: '22px', paddingBottom: '22px',
paddingTop: '22px', paddingTop: '22px',
...style,
}} }}
maxHeight="44" maxHeight={`${COLLECTION_ROW_HEIGHT}`}
as="li" as="li"
onMouseEnter={toggleHovered} onMouseEnter={toggleHovered}
onMouseLeave={toggleHovered} onMouseLeave={toggleHovered}
......
...@@ -7,20 +7,14 @@ import { Center, Column, Row } from 'nft/components/Flex' ...@@ -7,20 +7,14 @@ import { Center, Column, Row } from 'nft/components/Flex'
import { CrossIcon } from 'nft/components/icons' import { CrossIcon } from 'nft/components/icons'
import { FilterSidebar } from 'nft/components/profile/view/FilterSidebar' import { FilterSidebar } from 'nft/components/profile/view/FilterSidebar'
import { subhead } from 'nft/css/common.css' import { subhead } from 'nft/css/common.css'
import { import { useBag, useFiltersExpanded, useIsMobile, useSellAsset, useWalletCollections } from 'nft/hooks'
useBag, import { useWalletBalance } from 'nft/hooks'
useFiltersExpanded,
useIsMobile,
useSellAsset,
useWalletBalance,
useWalletCollections,
} from 'nft/hooks'
import { ScreenBreakpointsPaddings } from 'nft/pages/collection/index.css' import { ScreenBreakpointsPaddings } from 'nft/pages/collection/index.css'
import { OSCollectionsFetcher } from 'nft/queries' import { OSCollectionsFetcher } from 'nft/queries'
import { WalletCollection } from 'nft/types' import { WalletCollection } from 'nft/types'
import { Dispatch, SetStateAction, useEffect, useState } from 'react' import { Dispatch, SetStateAction, useEffect, useMemo, useState } from 'react'
import InfiniteScroll from 'react-infinite-scroll-component' import InfiniteScroll from 'react-infinite-scroll-component'
import { useQuery } from 'react-query' import { useInfiniteQuery } from 'react-query'
import { easings, useSpring } from 'react-spring' import { easings, useSpring } from 'react-spring'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
import shallow from 'zustand/shallow' import shallow from 'zustand/shallow'
...@@ -49,6 +43,7 @@ const ProfileHeader = styled.div` ...@@ -49,6 +43,7 @@ const ProfileHeader = styled.div`
` `
export const DEFAULT_WALLET_ASSET_QUERY_AMOUNT = 25 export const DEFAULT_WALLET_ASSET_QUERY_AMOUNT = 25
const WALLET_COLLECTIONS_PAGINATION_LIMIT = 300
const FILTER_SIDEBAR_WIDTH = 300 const FILTER_SIDEBAR_WIDTH = 300
const PADDING = 16 const PADDING = 16
...@@ -72,13 +67,33 @@ export const ProfilePage = () => { ...@@ -72,13 +67,33 @@ export const ProfilePage = () => {
const isMobile = useIsMobile() const isMobile = useIsMobile()
const [currentTokenPlayingMedia, setCurrentTokenPlayingMedia] = useState<string | undefined>() const [currentTokenPlayingMedia, setCurrentTokenPlayingMedia] = useState<string | undefined>()
const { data: ownerCollections } = useQuery( const getOwnerCollections = async ({ pageParam = 0 }) => {
['ownerCollections', address], const res = await OSCollectionsFetcher({
() => OSCollectionsFetcher({ params: { asset_owner: address, offset: '0', limit: '300' } }), params: {
{ asset_owner: address,
refetchOnWindowFocus: false, offset: `${pageParam * WALLET_COLLECTIONS_PAGINATION_LIMIT}`,
limit: `${WALLET_COLLECTIONS_PAGINATION_LIMIT}`,
},
})
return {
data: res,
nextPage: pageParam + 1,
} }
) }
const {
data: ownerCollectionsData,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isSuccess,
} = useInfiniteQuery(['ownerCollections', { address }], getOwnerCollections, {
getNextPageParam: (lastGroup, _allGroups) => (lastGroup.data.length === 0 ? undefined : lastGroup.nextPage),
refetchInterval: 15000,
refetchIntervalInBackground: false,
refetchOnWindowFocus: false,
refetchOnMount: false,
})
const { const {
walletAssets: ownerAssets, walletAssets: ownerAssets,
...@@ -86,6 +101,11 @@ export const ProfilePage = () => { ...@@ -86,6 +101,11 @@ export const ProfilePage = () => {
hasNext, hasNext,
} = useNftBalanceQuery(address, collectionFilters, [], DEFAULT_WALLET_ASSET_QUERY_AMOUNT) } = useNftBalanceQuery(address, collectionFilters, [], DEFAULT_WALLET_ASSET_QUERY_AMOUNT)
const ownerCollections = useMemo(
() => (isSuccess ? ownerCollectionsData?.pages.map((page) => page.data).flat() : null),
[isSuccess, ownerCollectionsData]
)
useEffect(() => { useEffect(() => {
ownerCollections && setWalletCollections(ownerCollections) ownerCollections && setWalletCollections(ownerCollections)
}, [ownerCollections, setWalletCollections]) }, [ownerCollections, setWalletCollections])
...@@ -106,7 +126,12 @@ export const ProfilePage = () => { ...@@ -106,7 +126,12 @@ export const ProfilePage = () => {
<> <>
<ProfileHeader>My NFTs</ProfileHeader> <ProfileHeader>My NFTs</ProfileHeader>
<Row alignItems="flex-start" position="relative"> <Row alignItems="flex-start" position="relative">
<FilterSidebar /> <FilterSidebar
fetchNextPage={fetchNextPage}
hasNextPage={hasNextPage}
isFetchingNextPage={isFetchingNextPage}
walletCollections={walletCollections}
/>
{(!isMobile || !isFiltersExpanded) && ( {(!isMobile || !isFiltersExpanded) && (
<Column width="full"> <Column width="full">
......
...@@ -102,7 +102,7 @@ export const ViewMyNftsAsset = ({ ...@@ -102,7 +102,7 @@ export const ViewMyNftsAsset = ({
<Tooltip <Tooltip
text={ text={
<Box as="span" className={bodySmall} color="textPrimary"> <Box as="span" className={bodySmall} color="textPrimary">
{isSelected ? <Trans>Selected</Trans> : <Trans>Deselected</Trans>} {isSelected ? <Trans>Added to bag</Trans> : <Trans>Removed from bag</Trans>}
</Box> </Box>
} }
show={showTooltip} show={showTooltip}
......
...@@ -145,6 +145,7 @@ const zIndices = { ...@@ -145,6 +145,7 @@ const zIndices = {
modal: '1060', modal: '1060',
popover: '1070', popover: '1070',
tooltip: '1080', tooltip: '1080',
modalOverTooltip: '1090',
} }
export const vars = createGlobalTheme(':root', { export const vars = createGlobalTheme(':root', {
......
...@@ -3823,7 +3823,15 @@ ...@@ -3823,7 +3823,15 @@
dependencies: dependencies:
"@types/react" "*" "@types/react" "*"
"@types/react-window@^1.8.2": "@types/react-window-infinite-loader@^1.0.6":
version "1.0.6"
resolved "https://registry.yarnpkg.com/@types/react-window-infinite-loader/-/react-window-infinite-loader-1.0.6.tgz#d7b23b4afaa1e0e2050876b766c3ea19f748f549"
integrity sha512-V8g8sBDLVeJJAfEENJS7VXZK+DRJ+jzPNtk8jpj2G+obhf+iqGNUDGwNWCbBhLiD+KpHhf3kWQlKBRi0tAeU4Q==
dependencies:
"@types/react" "*"
"@types/react-window" "*"
"@types/react-window@*", "@types/react-window@^1.8.2":
version "1.8.5" version "1.8.5"
resolved "https://registry.yarnpkg.com/@types/react-window/-/react-window-1.8.5.tgz#285fcc5cea703eef78d90f499e1457e9b5c02fc1" resolved "https://registry.yarnpkg.com/@types/react-window/-/react-window-1.8.5.tgz#285fcc5cea703eef78d90f499e1457e9b5c02fc1"
integrity sha512-V9q3CvhC9Jk9bWBOysPGaWy/Z0lxYcTXLtLipkt2cnRj1JOSFNF7wqGpkScSXMgBwC+fnVRg/7shwgddBG5ICw== integrity sha512-V9q3CvhC9Jk9bWBOysPGaWy/Z0lxYcTXLtLipkt2cnRj1JOSFNF7wqGpkScSXMgBwC+fnVRg/7shwgddBG5ICw==
...@@ -15093,6 +15101,11 @@ react-virtualized-auto-sizer@^1.0.2: ...@@ -15093,6 +15101,11 @@ react-virtualized-auto-sizer@^1.0.2:
resolved "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.5.tgz" resolved "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.5.tgz"
integrity sha512-kivjYVWX15TX2IUrm8F1jaCEX8EXrpy3DD+u41WGqJ1ZqbljWpiwscV+VxOM1l7sSIM1jwi2LADjhhAJkJ9dxA== integrity sha512-kivjYVWX15TX2IUrm8F1jaCEX8EXrpy3DD+u41WGqJ1ZqbljWpiwscV+VxOM1l7sSIM1jwi2LADjhhAJkJ9dxA==
react-window-infinite-loader@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/react-window-infinite-loader/-/react-window-infinite-loader-1.0.8.tgz#f88b1444829e732a3f4c5ae861644815329ddd88"
integrity sha512-907ZLAiZZfBHuZyiY0V7uiSL4P/rI6UQyCF9wES1cDWTeyNLgGLaxu+BZkcUW3R5tSCQcbCcWBl0jVIpYzrKGQ==
react-window@^1.8.5: react-window@^1.8.5:
version "1.8.6" version "1.8.6"
resolved "https://registry.npmjs.org/react-window/-/react-window-1.8.6.tgz" resolved "https://registry.npmjs.org/react-window/-/react-window-1.8.6.tgz"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment