Skip to content
Merged
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
67 changes: 49 additions & 18 deletions packages/common/src/api/tan-query/lineups/useForYouFeed.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { Id } from '@audius/sdk'
import { EntityType, Id } from '@audius/sdk'
import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query'

import { userTrackMetadataFromSDK } from '~/adapters/track'
import { transformAndCleanList } from '~/adapters/utils'
import { primeTrackData, useQueryContext } from '~/api/tan-query/utils'
import { ID } from '~/models'
import { transformAndCleanList, userFeedItemFromSDK } from '~/adapters'
import { useQueryContext } from '~/api/tan-query/utils'
import { ID, UserCollectionMetadata, UserTrackMetadata } from '~/models'

import { QUERY_KEYS } from '../queryKeys'
import { QueryKey, QueryOptions } from '../types'
import { LineupData, QueryKey, QueryOptions } from '../types'
import { useCurrentUserId } from '../users/account/useCurrentUserId'
import { makeLoadNextPage } from '../utils/infiniteQueryLoadNextPage'
import { primeCollectionData } from '../utils/primeCollectionData'
import { primeTrackData } from '../utils/primeTrackData'

export const FOR_YOU_INITIAL_PAGE_SIZE = 10
export const FOR_YOU_LOAD_MORE_PAGE_SIZE = 10
Expand All @@ -20,13 +21,16 @@ type ForYouFeedArgs = {
}

export const getForYouFeedQueryKey = (userId: ID | null | undefined) => {
return [QUERY_KEYS.forYouFeed, userId] as unknown as QueryKey<ID[]>
return [QUERY_KEYS.forYouFeed, userId] as unknown as QueryKey<LineupData[]>
}

/**
* "For You" feed for the Feed page. Backed by the dedicated
* `GET /v1/users/{id}/feed/for-you` endpoint — a lean 3-source pipeline
* (in-network, trending, underground) with linear ranking and diversity pass.
* `GET /v1/users/{id}/feed/for-you` endpoint — a lean 6-source pipeline
* (in-network, trending, underground × tracks + playlists) with linear
* ranking and a shared per-owner diversity pass. Returns a heterogenous
* feed of tracks and playlists/albums, mirroring the chronological feed
* shape; consumers that only render tracks can use `trackIds`.
*/
export const useForYouFeed = (
{
Expand All @@ -43,15 +47,15 @@ export const useForYouFeed = (

const query = useInfiniteQuery({
initialPageParam: 0,
getNextPageParam: (lastPage: ID[], allPages) => {
getNextPageParam: (lastPage: LineupData[], allPages) => {
const isFirstPage = allPages.length === 1
const currentPageSize = isFirstPage ? initialPageSize : loadMorePageSize
if (lastPage.length < currentPageSize) return undefined
return allPages.reduce((total, page) => total + page.length, 0)
},
queryKey,
queryFn: async ({ pageParam }) => {
if (!currentUserId) return [] as ID[]
queryFn: async ({ pageParam }): Promise<LineupData[]> => {
if (!currentUserId) return []
const isFirstPage = pageParam === 0
const currentPageSize = isFirstPage ? initialPageSize : loadMorePageSize
const sdk = await audiusSdk()
Expand All @@ -62,26 +66,53 @@ export const useForYouFeed = (
offset: pageParam
})

const tracks = primeTrackData({
tracks: transformAndCleanList(data, userTrackMetadataFromSDK),
queryClient
})
const feed = transformAndCleanList(data, userFeedItemFromSDK).map(
({ item }) => item
)
if (feed === null) return []

const { tracks, collections } = feed.reduce(
(acc, item) => {
if ('track_id' in item) {
acc.tracks.push(item)
} else {
acc.collections.push(item)
}
return acc
},
{
tracks: [] as UserTrackMetadata[],
collections: [] as UserCollectionMetadata[]
}
)

// Prime caches so tile renders don't have to re-fetch
primeTrackData({ tracks, queryClient })
primeCollectionData({ collections, queryClient })

return tracks.map(({ track_id }) => track_id)
return feed.map((item) =>
'track_id' in item
? { id: item.track_id, type: EntityType.TRACK }
: { id: item.playlist_id, type: EntityType.PLAYLIST }
)
},
select: (data) => data?.pages.flat(),
...options,
enabled: options?.enabled !== false && currentUserId !== null
})

const trackIds = query.data ?? []
const data = query.data ?? []
const trackIds = data
.filter((d) => d.type === EntityType.TRACK)
.map((d) => d.id as ID)

// When the query is disabled, react-query keeps isPending/isLoading true
// (data is undefined). Surface them as false so consumers can render an
// empty state instead of an indefinite loading state.
const isDisabled = currentUserId === null || options?.enabled === false

return {
data,
trackIds,
isPending: isDisabled ? false : query.isPending,
isLoading: isDisabled ? false : query.isLoading,
Expand Down
10 changes: 5 additions & 5 deletions packages/sdk/src/sdk/api/generated/default/apis/UsersApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4182,10 +4182,10 @@ export class UsersApi extends runtime.BaseAPI {

/**
* @hidden
* Returns a personalized For You feed for the user identified in the path. Twitter-style multi-source pipeline — candidate retrieval (in-network, trending, underground) → linear ranking (recency decay × engagement × social affinity, weighted by source) → diversity (per-artist cap + consecutive-same-artist lookahead).
* Returns a personalized For You feed for the user identified in the path. The response is a heterogenous list of tracks and playlists/albums (`{type, timestamp, item}` envelope), mirroring the chronological /v1/users/{id}/feed shape so clients can render both in a single ranked list. Twitter-style multi-source pipeline — candidate retrieval (in-network, trending, underground) → linear ranking (recency decay × engagement × social affinity, weighted by source) → diversity (shared per-artist cap across tracks + collections + consecutive-same-artist lookahead).
* Get For You feed for user
*/
async getUserForYouFeedRaw(params: GetUserForYouFeedRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<Tracks>> {
async getUserForYouFeedRaw(params: GetUserForYouFeedRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<UserFeedResponse>> {
if (params.id === null || params.id === undefined) {
throw new runtime.RequiredError('id','Required parameter params.id was null or undefined when calling getUserForYouFeed.');
}
Expand Down Expand Up @@ -4224,14 +4224,14 @@ export class UsersApi extends runtime.BaseAPI {
query: queryParameters,
}, initOverrides);

return new runtime.JSONApiResponse(response, (jsonValue) => TracksFromJSON(jsonValue));
return new runtime.JSONApiResponse(response, (jsonValue) => UserFeedResponseFromJSON(jsonValue));
}

/**
* Returns a personalized For You feed for the user identified in the path. Twitter-style multi-source pipeline — candidate retrieval (in-network, trending, underground) → linear ranking (recency decay × engagement × social affinity, weighted by source) → diversity (per-artist cap + consecutive-same-artist lookahead).
* Returns a personalized For You feed for the user identified in the path. The response is a heterogenous list of tracks and playlists/albums (`{type, timestamp, item}` envelope), mirroring the chronological /v1/users/{id}/feed shape so clients can render both in a single ranked list. Twitter-style multi-source pipeline — candidate retrieval (in-network, trending, underground) → linear ranking (recency decay × engagement × social affinity, weighted by source) → diversity (shared per-artist cap across tracks + collections + consecutive-same-artist lookahead).
* Get For You feed for user
*/
async getUserForYouFeed(params: GetUserForYouFeedRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<Tracks> {
async getUserForYouFeed(params: GetUserForYouFeedRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<UserFeedResponse> {
const response = await this.getUserForYouFeedRaw(params, initOverrides);
return await response.value();
}
Expand Down
Loading