Skip to content
This repository has been archived by the owner on Oct 4, 2023. It is now read-only.

Commit

Permalink
[C-2829] Finalize SuggestedTracks (#3706)
Browse files Browse the repository at this point in the history
  • Loading branch information
dylanjeffers authored Jul 7, 2023
1 parent 8a4b7e8 commit 46f2974
Show file tree
Hide file tree
Showing 14 changed files with 540 additions and 169 deletions.
1 change: 1 addition & 0 deletions packages/common/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './collection'
export * from './user'
export * from './developerApps'
export * from './suggestedTracks'
export * from './trending'
4 changes: 3 additions & 1 deletion packages/common/src/api/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { developerAppsApiReducer } from './developerApps'
import { favoritesApiReducer } from './favorites'
import { relatedArtistsApiReducer } from './relatedArtists'
import { trackApiReducer } from './track'
import { trendingApiReducer } from './trending'
import { userApiReducer } from './user'

export default combineReducers({
Expand All @@ -13,5 +14,6 @@ export default combineReducers({
trackApi: trackApiReducer,
userApi: userApiReducer,
developerAppsApi: developerAppsApiReducer,
favoritesApi: favoritesApiReducer
favoritesApi: favoritesApiReducer,
trendingApi: trendingApiReducer
})
125 changes: 113 additions & 12 deletions packages/common/src/api/suggestedTracks.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,132 @@
import { useMemo } from 'react'
import { useCallback, useEffect, useState } from 'react'

import { sampleSize } from 'lodash'
import { useSelector } from 'react-redux'
import { difference, shuffle } from 'lodash'
import { useSelector, useDispatch } from 'react-redux'

import { usePaginatedQuery } from 'audius-query'
import { ID } from 'models/Identifiers'
import { Status } from 'models/Status'
import { TimeRange } from 'models/TimeRange'
import { getUserId } from 'store/account/selectors'
import { addTrackToPlaylist } from 'store/cache/collections/actions'
import { getCollection } from 'store/cache/collections/selectors'
import { getTrack } from 'store/cache/tracks/selectors'
import { CommonState } from 'store/index'

import { useGetFavoritedTrackList } from './favorites'
import { useGetTracksByIds } from './track'
import { useGetTrending } from './trending'

export const useGetSuggestedTracks = () => {
const suggestedTrackCount = 5

const selectSuggestedTracks = (state: CommonState, ids: ID[]) => {
return ids.map((id) => {
const track = getTrack(state, { id })
if (!track) return { id, isLoading: true as const }
return { id, track, isLoading: false as const }
})
}

const selectCollectionTrackIds = (state: CommonState, collectionId: ID) => {
const collection = getCollection(state, { id: collectionId })
if (!collection) return []
return collection?.playlist_contents.track_ids.map((trackId) => trackId.track)
}

export const useGetSuggestedTracks = (collectionId: ID) => {
const currentUserId = useSelector(getUserId)
const { data: favoritedTracks } = useGetFavoritedTrackList(
{ currentUserId },
{ disabled: !currentUserId }
const dispatch = useDispatch()
const [suggestedTrackIds, setSuggestedTrackIds] = useState<ID[]>([])

const collectionTrackIds = useSelector((state: CommonState) =>
selectCollectionTrackIds(state, collectionId)
)

const favoritedTracksSample = useMemo(() => {
return sampleSize(favoritedTracks, 5)
const { data: favoritedTracks, status: favoritedStatus } =
useGetFavoritedTrackList({ currentUserId }, { disabled: !currentUserId })

useEffect(() => {
if (favoritedTracks) {
const suggestedTrackIds = difference(
shuffle(favoritedTracks).map((track) => track.save_item_id),
collectionTrackIds
)
setSuggestedTrackIds(suggestedTrackIds)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [favoritedTracks])

return useGetTracksByIds(
const {
data: trendingTracks,
status: trendingStatus,
loadMore
} = usePaginatedQuery(
useGetTrending,
{
timeRange: TimeRange.WEEK,
currentUserId,
genre: null
},
{
pageSize: 10,
disabled: favoritedStatus !== Status.SUCCESS
}
)

useEffect(() => {
if (trendingStatus === Status.SUCCESS) {
const trendingTrackIds = difference(
trendingTracks.map((track) => track.track_id),
collectionTrackIds
)
setSuggestedTrackIds([...suggestedTrackIds, ...trendingTrackIds])
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [trendingStatus])

useEffect(() => {
if (suggestedTrackIds.length < 5) {
loadMore()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [suggestedTrackIds.length])

const suggestedTracks = useSelector((state: CommonState) =>
selectSuggestedTracks(
state,
suggestedTrackIds.slice(0, suggestedTrackCount)
)
)

useGetTracksByIds(
{
currentUserId,
ids: favoritedTracksSample?.map((favorite) => favorite.save_item_id)
ids: suggestedTracks
.filter((suggestedTrack) => suggestedTrack.isLoading)
.map((suggestedTrack) => suggestedTrack.id)
},
{
disabled: !currentUserId || favoritedTracksSample.length === 0
disabled: !currentUserId || suggestedTrackIds.length === 0
}
)

const handleAddTrack = useCallback(
(trackId: ID, collectionId: ID) => {
dispatch(addTrackToPlaylist(trackId, collectionId))
const trackIndexToRemove = suggestedTrackIds.indexOf(trackId)
suggestedTrackIds.splice(trackIndexToRemove, 1)
setSuggestedTrackIds(suggestedTrackIds)
},
[dispatch, suggestedTrackIds]
)

const handleRefresh = useCallback(() => {
setSuggestedTrackIds(suggestedTrackIds.slice(suggestedTrackCount))
}, [suggestedTrackIds])

return {
suggestedTracks,
onRefresh: handleRefresh,
onAddTrack: handleAddTrack
}
}
32 changes: 32 additions & 0 deletions packages/common/src/api/trending.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { createApi } from 'audius-query'
import { ID } from 'models/Identifiers'
import { Kind } from 'models/Kind'
import { TimeRange } from 'models/TimeRange'
import { Genre } from 'utils/genres'
import { Nullable } from 'utils/typeUtils'

type GetTrendingArgs = {
timeRange: TimeRange
genre: Nullable<Genre>
offset: number
limit: number
currentUserId: Nullable<ID>
}

const trendingApi = createApi({
reducerPath: 'trendingApi',
endpoints: {
getTrending: {
fetch: async (args: GetTrendingArgs, { apiClient }) => {
return await apiClient.getTrending(args)
},
options: {
kind: Kind.TRACKS,
schemaKey: 'tracks'
}
}
}
})

export const { useGetTrending } = trendingApi.hooks
export const trendingApiReducer = trendingApi.reducer
50 changes: 34 additions & 16 deletions packages/common/src/audius-query/hooks/usePaginatedQuery.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,42 @@
import { useEffect, useState } from 'react'
import { useCallback, useEffect, useState } from 'react'

import { isEqual } from 'lodash'
import { useCustomCompareEffect } from 'react-use'

import { Status } from 'models/Status'

import { QueryHookResults } from '../types'
import { QueryHookOptions, QueryHookResults } from '../types'

export const usePaginatedQuery = <
Data extends [],
ArgsType extends { limit: number; offset: number },
HookType extends (args: ArgsType) => QueryHookResults<Data>
Data,
ArgsType extends { limit: number; offset: number }
>(
useQueryHook: HookType,
baseArgs: Exclude<ArgsType, 'limit' | 'offset'>,
pageSize: number
useQueryHook: (
args: ArgsType,
options?: QueryHookOptions
) => QueryHookResults<Data[]>,
baseArgs: Omit<ArgsType, 'limit' | 'offset'>,
options: { pageSize: number } & QueryHookOptions
) => {
const { pageSize, ...queryHookOptions } = options
const { disabled } = queryHookOptions
const [page, setPage] = useState(0)
const args = { ...baseArgs, limit: pageSize, offset: page * pageSize }
const result = useQueryHook(args)
const args = {
...baseArgs,
limit: pageSize,
offset: page * pageSize
} as ArgsType
const result = useQueryHook(args, queryHookOptions)

const loadMore = useCallback(() => {
if (!disabled) {
setPage(page + 1)
}
}, [disabled, page])

return {
...result,
loadMore: () => setPage(page + 1),
loadMore,
hasMore:
result.status === Status.IDLE ||
(!result.data && result.status === Status.LOADING) ||
Expand All @@ -31,21 +46,24 @@ export const usePaginatedQuery = <

export const useAllPaginatedQuery = <
Data,
ArgsType extends { limit: number; offset: number },
HookType extends (args: ArgsType) => QueryHookResults<Data[]>
ArgsType extends { limit: number; offset: number }
>(
useQueryHook: HookType,
useQueryHook: (
args: ArgsType,
options?: QueryHookOptions
) => QueryHookResults<Data[]>,
baseArgs: Omit<ArgsType, 'limit' | 'offset'>,
pageSize: number
options: { pageSize: number } & QueryHookOptions
) => {
const { pageSize, ...queryHookOptions } = options
const [page, setPage] = useState(0)
const [allData, setAllData] = useState<Data[]>([])
const args = {
...baseArgs,
limit: pageSize,
offset: page * pageSize
} as ArgsType
const result = useQueryHook(args)
const result = useQueryHook(args, queryHookOptions)
useEffect(() => {
if (result.status !== Status.SUCCESS) return
setAllData((allData) => [...allData, ...result.data])
Expand Down
Loading

0 comments on commit 46f2974

Please sign in to comment.