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
2 changes: 2 additions & 0 deletions packages/common/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export * from './tan-query/lineups/useTrendingPlaylists'
export * from './tan-query/lineups/useTrendingUnderground'
export * from './tan-query/lineups/useTrackPageLineup'
export * from './tan-query/lineups/useLineupQuery'
export * from './tan-query/lineups/useExclusiveTracks'

// Notifications
export * from './tan-query/notifications/useMarkNotificationsAsViewed'
Expand Down Expand Up @@ -162,6 +163,7 @@ export * from './tan-query/wallets/useUSDCBalance'
export * from './tan-query/wallets/useExternalWalletBalance'
export * from './tan-query/wallets/useCoinBalance'
export * from './tan-query/wallets/useCoinBalanceBreakdown'
export * from './tan-query/wallets/useUserBalanceHistory'
export * from './tan-query/wallets/useSendCoins'
export * from './tan-query/jupiter/useSwapCoins'
export * from './tan-query/jupiter/useCoinExchangeRate'
Expand Down
149 changes: 149 additions & 0 deletions packages/common/src/api/tan-query/lineups/useExclusiveTracks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { EntityType, OptionalId } from '@audius/sdk'
import {
InfiniteData,
useInfiniteQuery,
useQuery,
useQueryClient
} from '@tanstack/react-query'
import { useDispatch } from 'react-redux'

import { transformAndCleanList, userTrackMetadataFromSDK } from '~/adapters'
import { useQueryContext } from '~/api/tan-query/utils'
import { PlaybackSource } from '~/models/Analytics'
import { ID } from '~/models/Identifiers'
import {
exclusiveTracksPageSelectors,
exclusiveTracksPageLineupActions
} from '~/store/pages'

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

import { useLineupQuery } from './useLineupQuery'

const DEFAULT_PAGE_SIZE = 10

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just checking 10 was intentional?


type GateCondition = 'ungated' | 'usdc_purchase' | 'follow' | 'tip' | 'token'

type UseExclusiveTracksArgs = {
userId: ID | null | undefined
gateConditions?: GateCondition[]
pageSize?: number
initialPageSize?: number
}

export const getExclusiveTracksQueryKey = ({
userId,
gateConditions = ['token'],
pageSize
}: UseExclusiveTracksArgs) =>
[
QUERY_KEYS.exclusiveTracks,
userId,
{ gateConditions, pageSize }
] as unknown as QueryKey<InfiniteData<LineupData[]>>

export const useExclusiveTracks = (
{
userId,
gateConditions = ['token'],
pageSize = DEFAULT_PAGE_SIZE,
initialPageSize
}: UseExclusiveTracksArgs,
options?: QueryOptions
) => {
const { audiusSdk } = useQueryContext()
const { data: currentUserId } = useCurrentUserId()
const queryClient = useQueryClient()
const dispatch = useDispatch()

const queryData = useInfiniteQuery({
queryKey: getExclusiveTracksQueryKey({
userId,
gateConditions,
pageSize
}),
initialPageParam: 0,
getNextPageParam: (lastPage: LineupData[], allPages) => {
if (lastPage.length < pageSize) return undefined
return allPages.length * pageSize
},
queryFn: async ({ pageParam }) => {
const sdk = await audiusSdk()
const { data: tracks = [] } = await sdk.full.users.getTracksByUser({
id: OptionalId.parse(userId)!,
userId: OptionalId.parse(currentUserId),
gateCondition: gateConditions as any,
limit: pageSize,
offset: pageParam
})

const processedTracks = transformAndCleanList(
tracks,
userTrackMetadataFromSDK
)
primeTrackData({ tracks: processedTracks, queryClient })

// Update lineup when new data arrives
dispatch(
exclusiveTracksPageLineupActions.fetchLineupMetadatas(
pageParam,
pageSize,
false,
{ items: processedTracks }
)
)

return processedTracks.map((t) => ({
id: t.track_id,
type: EntityType.TRACK
}))
},
select: (data) => data?.pages.flat(),
...options,
enabled: options?.enabled !== false && !!userId
})

return useLineupQuery({
lineupData: queryData.data ?? [],
queryData,
queryKey: getExclusiveTracksQueryKey({
userId,
gateConditions,
pageSize
}),
lineupActions: exclusiveTracksPageLineupActions,
lineupSelector: exclusiveTracksPageSelectors.getLineup,
playbackSource: PlaybackSource.EXCLUSIVE_TRACKS_PAGE,
pageSize,
initialPageSize
})
}

// Hook to get the count of exclusive tracks
export const useExclusiveTracksCount = (args: {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this right? seems like we'd need a separate endpoint?

userId: ID | null | undefined
gateConditions?: GateCondition[]
enabled?: boolean
}) => {
const { userId, gateConditions = ['token'], enabled = true } = args
const { audiusSdk } = useQueryContext()
const { data: currentUserId } = useCurrentUserId()

return useQuery({
queryKey: [QUERY_KEYS.exclusiveTracksCount, userId, { gateConditions }],
queryFn: async () => {
const sdk = await audiusSdk()
const { data: count } = await sdk.full.users.getTracksCountByUser({
id: OptionalId.parse(userId)!,
userId: OptionalId.parse(currentUserId),
gateCondition: gateConditions as any
})

return count ?? 0
},
enabled: enabled && !!userId
})
}
4 changes: 3 additions & 1 deletion packages/common/src/api/tan-query/queryKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,5 +127,7 @@ export const QUERY_KEYS = {
walletSolBalance: 'walletSolBalance',
launchpadConfig: 'launchpadConfig',
externalWalletBalance: 'externalWalletBalance',
claimFee: 'claimFee'
claimFee: 'claimFee',
exclusiveTracks: 'exclusiveTracks',
exclusiveTracksCount: 'exclusiveTracksCount'
} as const
69 changes: 69 additions & 0 deletions packages/common/src/api/tan-query/wallets/useUserBalanceHistory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { useQuery } from '@tanstack/react-query'

import { QueryOptions } from '../types'
import { useCurrentUserId } from '../users/account/useCurrentUserId'

export type BalanceHistoryDataPoint = {
timestamp: number
balanceUsd: number
}

type UseUserBalanceHistoryParams = {
userId?: number
enabled?: boolean
}

/**
* Mock hook for getting user balance history (coins + USDC + AUDIO in USD).
* Returns hourly data points for the past week.
* TODO: Replace with real API call when backend is ready
*/
export const useUserBalanceHistory = (
params: UseUserBalanceHistoryParams = {},
options?: QueryOptions
) => {
const { data: currentUserId } = useCurrentUserId()
const userId = params.userId ?? currentUserId

return useQuery({
queryKey: ['userBalanceHistory', userId],
queryFn: async (): Promise<BalanceHistoryDataPoint[]> => {
// Mock data: Generate hourly data points for the past week (7 days * 24 hours = 168 data points)
const now = Date.now()
const oneHour = 60 * 60 * 1000
const oneWeek = 7 * 24 * oneHour
const startTime = now - oneWeek

const dataPoints: BalanceHistoryDataPoint[] = []

// Generate realistic-looking balance data with some variance
const baseBalance = 5000 // Base balance in USD
let previousBalance = baseBalance

for (let i = 0; i <= 168; i++) {
const timestamp = startTime + i * oneHour

// Add some realistic variance (±2% per hour with trend)
const randomVariance = (Math.random() - 0.48) * 0.02
const trendVariance = Math.sin(i / 20) * 0.01 // Slight upward/downward trend
const totalVariance = 1 + randomVariance + trendVariance

const newBalance = previousBalance * totalVariance
previousBalance = newBalance

dataPoints.push({
timestamp,
balanceUsd: Math.round(newBalance * 100) / 100 // Round to 2 decimals
})
}

// Simulate network delay
await new Promise((resolve) => setTimeout(resolve, 300))

return dataPoints
},
enabled: !!userId && (params.enabled ?? true) && (options?.enabled ?? true),
staleTime: 60000, // 1 minute
...options
})
}
3 changes: 2 additions & 1 deletion packages/common/src/models/Analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1527,7 +1527,8 @@ export enum PlaybackSource {
EMBED_PLAYER = 'embed player',
CHAT_TRACK = 'chat_track',
CHAT_PLAYLIST_TRACK = 'chat_playlist_track',
SEARCH_PAGE = 'search page'
SEARCH_PAGE = 'search page',
EXCLUSIVE_TRACKS_PAGE = 'exclusive tracks page'
}

type PlaybackPlay = {
Expand Down
4 changes: 4 additions & 0 deletions packages/common/src/store/pages/exclusive-tracks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { default as exclusiveTracksPageReducer } from './slice'
export * as exclusiveTracksPageActions from './slice'
export * as exclusiveTracksPageLineupActions from './lineup/actions'
export * as exclusiveTracksPageSelectors from './selectors'
11 changes: 11 additions & 0 deletions packages/common/src/store/pages/exclusive-tracks/lineup/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { LineupActions } from '~/store/lineup/actions'

export const PREFIX = 'EXCLUSIVE_TRACKS'

class TracksActions extends LineupActions {
constructor() {
super(PREFIX)
}
}

export const tracksActions = new TracksActions()
25 changes: 25 additions & 0 deletions packages/common/src/store/pages/exclusive-tracks/lineup/reducer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { RESET_SUCCEEDED, stripPrefix } from '~/store/lineup/actions'
import { initialLineupState } from '~/store/lineup/reducer'

import { PREFIX } from './actions'

export const initialState = {
...initialLineupState,
prefix: PREFIX
}

const actionsMap: { [key in string]: any } = {
[RESET_SUCCEEDED](_state: typeof initialState) {
const newState = initialState
return newState
}
}

const tracks = (state = initialState, action: { type: string }) => {
const baseActionType = stripPrefix(PREFIX, action.type)
const matchingReduceFunction = actionsMap[baseActionType]
if (!matchingReduceFunction) return state
return matchingReduceFunction(state, action)
}

export default tracks
7 changes: 7 additions & 0 deletions packages/common/src/store/pages/exclusive-tracks/selectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { CommonState } from '~/store/commonStore'

export const getBaseState = (state: CommonState) => state.pages.exclusiveTracks

export const getLineup = (state: CommonState) => getBaseState(state).tracks

export const getUserId = (state: CommonState) => getBaseState(state).page.userId
49 changes: 49 additions & 0 deletions packages/common/src/store/pages/exclusive-tracks/slice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { combineReducers } from 'redux'

import { asLineup } from '~/store/lineup/reducer'

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

import { PREFIX as exclusiveTracksPrefix } from './lineup/actions'
import exclusiveTracksReducer, {
initialState as initialLineupState
} from './lineup/reducer'

type State = {
userId: ID | null
tracks: typeof initialLineupState
}

const initialState: State = {
userId: null,
tracks: initialLineupState
}

const slice = createSlice({
name: 'application/pages/exclusiveTracks',
initialState,
reducers: {
reset: (state) => {
state.userId = null
},
setUserId: (state, action: PayloadAction<{ userId: ID }>) => {
const { userId } = action.payload
state.userId = userId
}
}
})

const exclusiveTracksLineupReducer = asLineup(
exclusiveTracksPrefix,
exclusiveTracksReducer
)

export const { reset, setUserId } = slice.actions

export default combineReducers({
page: slice.reducer,
tracks: exclusiveTracksLineupReducer
})

export const actions = slice.actions
11 changes: 11 additions & 0 deletions packages/common/src/store/pages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,14 @@ export * from './chat'
export { default as premiumTracksPageLineupReducer } from './premium-tracks/lineup/reducer'
export * as premiumTracksPageLineupSelectors from './premium-tracks/lineup/selectors'
export { premiumTracksActions as premiumTracksPageLineupActions } from './premium-tracks/lineup/actions'

export {
default as exclusiveTracksPageLineupReducer,
initialState as exclusiveTracksPageLineupInitialState
} from './exclusive-tracks/lineup/reducer'
export { tracksActions as exclusiveTracksPageLineupActions } from './exclusive-tracks/lineup/actions'
export {
default as exclusiveTracksPageReducer,
actions as exclusiveTracksPageActions
} from './exclusive-tracks/slice'
export * as exclusiveTracksPageSelectors from './exclusive-tracks/selectors'
3 changes: 3 additions & 0 deletions packages/common/src/store/reducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
deactivateAccountReducer,
DeactivateAccountState
} from './pages/deactivate-account'
import exclusiveTracks from './pages/exclusive-tracks/slice'
import feed from './pages/feed/reducer'
import { FeedPageState } from './pages/feed/types'
import historyPageReducer from './pages/history-page/reducer'
Expand Down Expand Up @@ -195,6 +196,7 @@ export const reducers = (storage: Storage, history?: History) => ({
trendingUnderground,
settings,
remixes,
exclusiveTracks,
premiumTracks
}),
search: searchReducer(storage),
Expand Down Expand Up @@ -301,6 +303,7 @@ export type CommonState = {
trendingPlaylists: ReturnType<typeof trendingPlaylists>
trendingUnderground: ReturnType<typeof trendingUnderground>
remixes: ReturnType<typeof remixes>
exclusiveTracks: ReturnType<typeof exclusiveTracks>
premiumTracks: ReturnType<typeof premiumTracks>
}
search: SearchState
Expand Down
Loading