Skip to content

Commit

Permalink
[PAY-1202] Refactor saved collections fetching (#3337)
Browse files Browse the repository at this point in the history
  • Loading branch information
schottra authored May 16, 2023
1 parent a0bdad5 commit 3c9b0f1
Show file tree
Hide file tree
Showing 27 changed files with 572 additions and 224 deletions.
1 change: 1 addition & 0 deletions packages/common/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ export * from './useLinkUnfurlMetadata'
export * from './useThrottledCallback'
export * from './useDebouncedCallback'
export * from './useSetInboxPermissions'
export * from './useSavedCollections'
84 changes: 84 additions & 0 deletions packages/common/src/hooks/useSavedCollections.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { useCallback, useEffect, useState } from 'react'

import { useDispatch, useSelector } from 'react-redux'

import { Status } from 'models/Status'

import { accountActions } from '../store/account'
import {
savedCollectionsActions,
savedCollectionsSelectors
} from '../store/saved-collections'

const { fetchSavedPlaylists } = accountActions
const { fetchCollections } = savedCollectionsActions

const {
getAccountAlbums,
getSavedAlbumsState,
getFetchedAlbumsWithDetails,
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 = {
pageSize?: number
}
export function useSavedAlbumsDetails({
pageSize = DEFAULT_PAGE_SIZE
}: UseSavedAlbumDetailsConfig) {
const dispatch = useDispatch()
const [hasFetched, setHasFetched] = useState(false)
const { unfetched: unfetchedAlbums, fetched: albumsWithDetails } =
useSelector(getFetchedAlbumsWithDetails)
const { status } = useSelector(getSavedAlbumsState)

const fetchMore = useCallback(() => {
if (status === Status.LOADING || unfetchedAlbums.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()

useEffect(() => {
dispatch(fetchSavedPlaylists())
}, [dispatch])
}
1 change: 1 addition & 0 deletions packages/common/src/store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,4 @@ export * from './premium-content'
export * from './collectibles'
export * from './solana'
export * from './playlist-updates'
export * from './saved-collections'
4 changes: 4 additions & 0 deletions packages/common/src/store/reducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import { recoveryEmailReducer, RecoveryEmailState } from './recovery-email'
import remixSettingsReducer, {
RemixSettingsState
} from './remix-settings/slice'
import savedCollectionsReducer from './saved-collections/slice'
import solanaReducer from './solana/slice'
import stemsUpload from './stems-upload/slice'
import tippingReducer from './tipping/slice'
Expand Down Expand Up @@ -149,6 +150,8 @@ export const reducers = () => ({
// @ts-ignore
users: asCache(usersReducer, Kind.USERS),

savedCollections: savedCollectionsReducer,

// Playback
queue,
player,
Expand Down Expand Up @@ -268,6 +271,7 @@ export type CommonState = {

// TODO: missing types for internally managed api slice state
api: any
savedCollections: ReturnType<typeof savedCollectionsReducer>

// Playback
queue: ReturnType<typeof queue>
Expand Down
6 changes: 6 additions & 0 deletions packages/common/src/store/saved-collections/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export {
default as savedCollectionsReducer,
actions as savedCollectionsActions
} from './slice'
export * as savedCollectionsSelectors from './selectors'
export * from './types'
58 changes: 58 additions & 0 deletions packages/common/src/store/saved-collections/selectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { createSelector } from '@reduxjs/toolkit'

import { getUsers } from 'store/cache/users/selectors'

import { AccountCollection } from '../account'
import { getAccountStatus } from '../account/selectors'
import { getCollections } from '../cache/collections/selectors'
import { CommonState } from '../commonStore'

import { CollectionWithOwner } from './types'

const getAccountCollections = (state: CommonState) => state.account.collections
export const getSavedAlbumsState = (state: CommonState) =>
state.savedCollections.albums
export const getSavedPlaylistsState = (state: CommonState) =>
state.savedCollections.playlists

export const getAccountAlbums = createSelector(
[getAccountCollections, getAccountStatus],
(collections, status) => ({
status,
data: Object.values(collections).filter((c) => c.is_album)
})
)

type GetAlbumsWithDetailsResult = {
fetched: CollectionWithOwner[]
unfetched: AccountCollection[]
}
/** Returns a mapped list of albums for which we have fetched full details */
export const getFetchedAlbumsWithDetails = createSelector(
[getAccountAlbums, getCollections, getUsers],
(albums, collections, users) => {
// TODO: Might want to read status, what happens for failed loads of parts of the collection?
return albums.data.reduce<GetAlbumsWithDetailsResult>(
(acc, cur) => {
const collectionMetadata = collections[cur.id]
if (collectionMetadata) {
const ownerHandle =
users[collectionMetadata.playlist_owner_id]?.handle ?? ''
acc.fetched.push({ ...collections[cur.id], ownerHandle })
} else {
acc.unfetched.push(cur)
}
return acc
},
{ fetched: [], unfetched: [] }
)
}
)

export const getAccountPlaylists = createSelector(
[getAccountCollections, getAccountStatus],
(collections, status) => ({
status,
data: Object.values(collections).filter((c) => !c.is_album)
})
)
72 changes: 72 additions & 0 deletions packages/common/src/store/saved-collections/slice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'

import { ID } from '../../models/Identifiers'
import { Status } from '../../models/Status'

import { CollectionType } from './types'

type FetchCollectionsPayload = {
type: CollectionType
ids: ID[]
}

type FetchCollectionsSucceededPayload = {
type: CollectionType
}

type FetchCollectionsFailedPayload = {
type: CollectionType
}

export type CollectionListState = {
status: Status
}

export type SavedCollectionsState = {
albums: CollectionListState
playlists: CollectionListState
}

const initialState: SavedCollectionsState = {
albums: {
status: Status.IDLE
},
playlists: {
status: Status.IDLE
}
}

const slice = createSlice({
name: 'saved-collections',
initialState,
reducers: {
fetchCollections: (
state,
action: PayloadAction<FetchCollectionsPayload>
) => {
state[action.payload.type].status = Status.LOADING
},
fetchCollectionsSucceeded: (
state,
action: PayloadAction<FetchCollectionsSucceededPayload>
) => {
const list = state[action.payload.type]
list.status = Status.SUCCESS
},
fetchCollectionsFailed: (
state,
action: PayloadAction<FetchCollectionsFailedPayload>
) => {
state[action.payload.type].status = Status.ERROR
}
}
})

export const {
fetchCollections,
fetchCollectionsSucceeded,
fetchCollectionsFailed
} = slice.actions

export const actions = slice.actions
export default slice.reducer
7 changes: 7 additions & 0 deletions packages/common/src/store/saved-collections/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Collection } from '../../models/Collection'

export type CollectionType = 'albums' | 'playlists'

export type CollectionWithOwner = Collection & {
ownerHandle: string
}
9 changes: 5 additions & 4 deletions packages/web/src/common/store/account/sagas.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@ import {
import { call, put, fork, select, takeEvery } from 'redux-saga/effects'

import { identify } from 'common/store/analytics/actions'
import { retrieveCollections } from 'common/store/cache/collections/utils'
import { addPlaylistsNotInLibrary } from 'common/store/playlist-library/sagas'
import { updateProfileAsync } from 'common/store/profile/sagas'
import { waitForWrite, waitForRead } from 'utils/sagaHelpers'

import { retrieveCollections } from '../cache/collections/utils'

import disconnectedWallets from './disconnected_wallet_fix.json'

const { fetchProfile } = profilePageActions
Expand Down Expand Up @@ -364,7 +365,7 @@ function* fetchSavedAlbumsAsync() {
yield waitForRead()
const cachedSavedAlbums = yield select(getAccountAlbumIds)
if (cachedSavedAlbums.length > 0) {
yield call(retrieveCollections, null, cachedSavedAlbums)
yield call(retrieveCollections, cachedSavedAlbums)
}
}

Expand All @@ -375,15 +376,15 @@ function* fetchSavedPlaylistsAsync() {
yield fork(function* () {
const savedPlaylists = yield select(getAccountSavedPlaylistIds)
if (savedPlaylists.length > 0) {
yield call(retrieveCollections, null, savedPlaylists)
yield call(retrieveCollections, savedPlaylists)
}
})

// Fetch your own playlists
yield fork(function* () {
const ownPlaylists = yield select(getAccountOwnedPlaylistIds)
if (ownPlaylists.length > 0) {
yield call(retrieveCollections, null, ownPlaylists)
yield call(retrieveCollections, ownPlaylists)
}
})
}
Expand Down
8 changes: 3 additions & 5 deletions packages/web/src/common/store/cache/collections/sagas.js
Original file line number Diff line number Diff line change
Expand Up @@ -390,12 +390,10 @@ function* addTrackToPlaylistAsync(action) {

// Retrieve tracks with the the collection so we confirm with the
// most up-to-date information.
const { collections } = yield call(
retrieveCollections,
const { collections } = yield call(retrieveCollections, [action.playlistId], {
userId,
[action.playlistId],
true
)
fetchTracks: true
})
const playlist = collections[action.playlistId]

const trackUid = makeUid(
Expand Down
Loading

0 comments on commit 3c9b0f1

Please sign in to comment.