-
Notifications
You must be signed in to change notification settings - Fork 111
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
…le web (#3371)
- Loading branch information
Showing
11 changed files
with
278 additions
and
176 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,84 +1,92 @@ | ||
import { useCallback, useEffect, useState } from 'react' | ||
import { useCallback, useMemo } from 'react' | ||
|
||
import { useDispatch, useSelector } from 'react-redux' | ||
|
||
import { ID } from 'models/Identifiers' | ||
import { Status } from 'models/Status' | ||
import { CommonState } from 'store/index' | ||
import { getFetchedCollectionIds } from 'store/saved-collections/selectors' | ||
|
||
import { accountActions } from '../store/account' | ||
import { | ||
CollectionType, | ||
savedCollectionsActions, | ||
savedCollectionsSelectors | ||
} from '../store/saved-collections' | ||
|
||
const { fetchSavedPlaylists } = accountActions | ||
const { fetchCollections } = savedCollectionsActions | ||
|
||
const { | ||
getAccountAlbums, | ||
getSavedAlbumsState, | ||
getFetchedAlbumsWithDetails, | ||
getAccountPlaylists | ||
} = savedCollectionsSelectors | ||
const { getAccountAlbums, getSavedCollectionsState, getAccountPlaylists } = | ||
savedCollectionsSelectors | ||
|
||
const DEFAULT_PAGE_SIZE = 50 | ||
|
||
export function useSavedAlbums() { | ||
return useSelector(getAccountAlbums) | ||
} | ||
|
||
/* TODO: Handle filtering | ||
* Option 1: This hook takes the list of album ids to fetch and computes the unfetched | ||
* based on that. | ||
* Option 2: Bake filter into selectors which drive this. Downside: Can't use this in multiple places... | ||
*/ | ||
type UseSavedAlbumDetailsConfig = { | ||
export function useSavedPlaylists() { | ||
return useSelector(getAccountPlaylists) | ||
} | ||
|
||
type UseFetchedCollectionsConfig = { | ||
collectionIds: ID[] | ||
type: CollectionType | ||
pageSize?: number | ||
} | ||
export function useSavedAlbumsDetails({ | ||
|
||
type UseFetchedSavedCollectionsResult = { | ||
/** A list of IDs representing the subset of requested collections which have been fetched */ | ||
data: ID[] | ||
/** The current fetching state of the list of collections requested */ | ||
status: Status | ||
/** Whether any items remain unfetched */ | ||
hasMore: boolean | ||
/** Triggers fetching of the next page of items */ | ||
fetchMore: () => void | ||
} | ||
/** Given a list of collectionIds and a type ('albums' or 'playlists'), returns state | ||
* necessary to display a list of fully-fetched collections of that type, as well as | ||
* load any remaining items which haven't been fetched. | ||
*/ | ||
export function useFetchedSavedCollections({ | ||
collectionIds, | ||
type, | ||
pageSize = DEFAULT_PAGE_SIZE | ||
}: UseSavedAlbumDetailsConfig) { | ||
}: UseFetchedCollectionsConfig): UseFetchedSavedCollectionsResult { | ||
const dispatch = useDispatch() | ||
const [hasFetched, setHasFetched] = useState(false) | ||
const { unfetched: unfetchedAlbums, fetched: albumsWithDetails } = | ||
useSelector(getFetchedAlbumsWithDetails) | ||
const { status } = useSelector(getSavedAlbumsState) | ||
|
||
const { status } = useSelector((state: CommonState) => | ||
getSavedCollectionsState(state, type) | ||
) | ||
const fetchedCollectionIDs = useSelector(getFetchedCollectionIds) | ||
|
||
const { unfetched, fetched } = useMemo(() => { | ||
const fetchedSet = new Set(fetchedCollectionIDs) | ||
return collectionIds.reduce<{ fetched: ID[]; unfetched: ID[] }>( | ||
(accum, id) => { | ||
if (fetchedSet.has(id)) { | ||
accum.fetched.push(id) | ||
} else { | ||
accum.unfetched.push(id) | ||
} | ||
return accum | ||
}, | ||
{ fetched: [], unfetched: [] } | ||
) | ||
}, [collectionIds, fetchedCollectionIDs]) | ||
|
||
const fetchMore = useCallback(() => { | ||
if (status === Status.LOADING || unfetchedAlbums.length === 0) { | ||
if (status === Status.LOADING || unfetched.length === 0) { | ||
return | ||
} | ||
const ids = unfetchedAlbums | ||
.slice(0, Math.min(pageSize, unfetchedAlbums.length)) | ||
.map((c) => c.id) | ||
dispatch(fetchCollections({ type: 'albums', ids })) | ||
setHasFetched(true) | ||
}, [status, unfetchedAlbums, pageSize, dispatch, setHasFetched]) | ||
|
||
// Fetch first page if we don't have any items fetched yet | ||
// Needs to wait for at least some albums to be fetchable | ||
useEffect(() => { | ||
if ( | ||
!hasFetched && | ||
// TODO: This check should change once InfiniteScroll is implemented | ||
status !== Status.LOADING /* && | ||
unfetchedAlbums.length > 0 && | ||
albumsWithDetails.length === 0 */ | ||
) { | ||
fetchMore() | ||
} | ||
}, [albumsWithDetails, status, hasFetched, unfetchedAlbums, fetchMore]) | ||
|
||
return { data: albumsWithDetails, status, fetchMore } | ||
} | ||
|
||
export function useSavedPlaylists() { | ||
return useSelector(getAccountPlaylists) | ||
} | ||
|
||
export function useSavedPlaylistsDetails() { | ||
const dispatch = useDispatch() | ||
const ids = unfetched.slice(0, Math.min(pageSize, unfetched.length)) | ||
dispatch(fetchCollections({ type, ids })) | ||
}, [status, unfetched, pageSize, type, dispatch]) | ||
|
||
useEffect(() => { | ||
dispatch(fetchSavedPlaylists()) | ||
}, [dispatch]) | ||
return { | ||
data: fetched, | ||
status, | ||
hasMore: unfetched.length > 0, | ||
fetchMore | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1 @@ | ||
import { Collection } from '../../models/Collection' | ||
|
||
export type CollectionType = 'albums' | 'playlists' | ||
|
||
export type CollectionWithOwner = Collection & { | ||
ownerHandle: string | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
5 changes: 5 additions & 0 deletions
5
packages/web/src/components/lineup/InfiniteCardLineup.module.css
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
.spinner { | ||
margin: auto; | ||
width: var(--unit-8); | ||
height: 100px; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import React, { useCallback, useRef } from 'react' | ||
|
||
import InfiniteScroll from 'react-infinite-scroller' | ||
|
||
import LoadingSpinner from 'components/loading-spinner/LoadingSpinner' | ||
import { getScrollParent } from 'utils/scrollParent' | ||
|
||
import CardLineup, { CardLineupProps } from './CardLineup' | ||
import styles from './InfiniteCardLineup.module.css' | ||
|
||
type InfiniteLoadingProps = { | ||
hasMore: boolean | ||
loadMore: () => void | ||
} | ||
|
||
export type InfiniteCardLineupProps = CardLineupProps & InfiniteLoadingProps | ||
|
||
const InfiniteCardLineup = (props: InfiniteCardLineupProps) => { | ||
const { hasMore, loadMore, ...lineupProps } = props | ||
const scrollRef = useRef(null) | ||
|
||
const getNearestScrollParent = useCallback(() => { | ||
if (!scrollRef.current) { | ||
return null | ||
} | ||
return ( | ||
(getScrollParent(scrollRef.current) as unknown as HTMLElement) ?? null | ||
) | ||
}, []) | ||
|
||
return ( | ||
<> | ||
<InfiniteScroll | ||
hasMore={hasMore} | ||
getScrollParent={getNearestScrollParent} | ||
loadMore={loadMore} | ||
loader={ | ||
<LoadingSpinner key='loading-spinner' className={styles.spinner} /> | ||
} | ||
useWindow={false} | ||
> | ||
{React.createElement(CardLineup, lineupProps)} | ||
</InfiniteScroll> | ||
<div ref={scrollRef} /> | ||
</> | ||
) | ||
} | ||
|
||
export default InfiniteCardLineup |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.