Skip to content

Commit

Permalink
[C-5738] favoriteTrack and unfavoriteTrack to tan-query (#11431)
Browse files Browse the repository at this point in the history
  • Loading branch information
amendelsohn authored Mar 3, 2025
1 parent db6a857 commit f4ee993
Show file tree
Hide file tree
Showing 40 changed files with 688 additions and 335 deletions.
2 changes: 1 addition & 1 deletion packages/common/src/adapters/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { userTrackMetadataFromSDK } from './track'
import { userMetadataFromSDK } from './user'
import { transformAndCleanList } from './utils'

type SearchResults = {
export type SearchResults = {
tracks: UserTrackMetadata[]
saved_tracks: UserTrackMetadata[]
users: UserMetadata[]
Expand Down
3 changes: 3 additions & 0 deletions packages/common/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,7 @@ export * from './tan-query/useNotificationValidTypes'
export * from './tan-query/useMarkNotificationsAsViewed'
export * from './tan-query/useUserByParams'
export * from './tan-query/useCollection'
export * from './tan-query/useFavoriteTrack'
export * from './tan-query/useUnfavoriteTrack'
export * from './tan-query/useToggleFavoriteTrack'
export * from './tan-query/types'
2 changes: 1 addition & 1 deletion packages/common/src/api/tan-query/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { UseLineupQueryData } from './utils/useLineupQuery'
*/
export type QueryOptions = Pick<
DefinedInitialDataOptions<any>,
'staleTime' | 'enabled' | 'placeholderData'
'staleTime' | 'enabled' | 'placeholderData' | 'select'
>

export type LineupQueryData = UseLineupQueryData &
Expand Down
171 changes: 171 additions & 0 deletions packages/common/src/api/tan-query/useFavoriteTrack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import { Id } from '@audius/sdk'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { useDispatch } from 'react-redux'

import { useAudiusQueryContext } from '~/audius-query'
import { useAppContext } from '~/context/appContext'
import { Name } from '~/models/Analytics'
import { Feature } from '~/models/ErrorReporting'
import { ID } from '~/models/Identifiers'
import { Track } from '~/models/Track'
import { accountActions } from '~/store/account'
import { tracksSocialActions } from '~/store/social'

import { useCurrentUserId } from './useCurrentUserId'
import { getTrackQueryKey } from './useTrack'
import { useUser } from './useUser'
import { primeTrackData } from './utils/primeTrackData'

type FavoriteTrackArgs = {
trackId: ID
source?: string
}

export const useFavoriteTrack = () => {
const { audiusSdk, reportToSentry } = useAudiusQueryContext()
const queryClient = useQueryClient()
const dispatch = useDispatch()
const { data: currentUserId } = useCurrentUserId()
const { data: currentUser } = useUser(currentUserId)
const {
analytics: { track: trackEvent }
} = useAppContext()

return useMutation({
mutationFn: async ({ trackId }: FavoriteTrackArgs) => {
if (!currentUserId) throw new Error('User ID is required')
const sdk = await audiusSdk()
await sdk.tracks.favoriteTrack({
trackId: Id.parse(trackId),
userId: Id.parse(currentUserId)
})
},
onMutate: async ({ trackId, source }) => {
if (!currentUserId || !currentUser) {
// TODO: throw toast and redirect to sign in
throw new Error('User ID is required')
}

// Cancel any outgoing refetches
await queryClient.cancelQueries({ queryKey: getTrackQueryKey(trackId) })

// Snapshot the previous values
const previousTrack = queryClient.getQueryData<Track>(
getTrackQueryKey(trackId)
)
if (!previousTrack) throw new Error('Track not found')

// Don't allow favoriting your own track
if (previousTrack.owner_id === currentUserId)
throw new Error('Cannot favorite your own track')

// Don't allow favoriting if already favorited
if (previousTrack.has_current_user_saved)
throw new Error('Track already favorited')

// Increment the save count
dispatch(accountActions.incrementTrackSaveCount())

// Track analytics event
trackEvent({
eventName: Name.FAVORITE,
properties: {
kind: 'track',
source,
id: trackId
}
})

// Optimistically update track data
const update: Partial<Track> = {
has_current_user_saved: true,
save_count: previousTrack.save_count + 1
}

// Handle co-sign logic for remixes
const remixTrack = previousTrack.remix_of?.tracks?.[0]
const isCoSign = remixTrack?.user?.user_id === currentUserId
if (remixTrack && isCoSign) {
const remixOf = {
tracks: [
{
...remixTrack,
has_remix_author_saved: true
}
]
}
update.remix_of = remixOf
update._co_sign = remixOf.tracks[0]
}

primeTrackData({
tracks: [{ ...previousTrack, ...update }],
queryClient,
dispatch,
forceReplace: true
})

return { previousTrack, previousUser: currentUser }
},
onSuccess: async (_, { trackId }) => {
// Handle co-sign events after successful save
const track = queryClient.getQueryData<Track>(getTrackQueryKey(trackId))
if (!track) return

const remixTrack = track.remix_of?.tracks?.[0]
const isCoSign = remixTrack?.user?.user_id === currentUserId
if (isCoSign) {
const parentTrackId = remixTrack.parent_track_id
const hasAlreadyCoSigned =
remixTrack.has_remix_author_reposted ||
remixTrack.has_remix_author_saved

const parentTrack = queryClient.getQueryData<Track>(
getTrackQueryKey(parentTrackId)
)

// Dispatch co-sign events
trackEvent({
eventName: Name.REMIX_COSIGN_INDICATOR,
properties: {
id: trackId,
handle: currentUser?.handle,
original_track_id: parentTrack?.track_id,
original_track_title: parentTrack?.title,
action: 'favorited'
}
})

if (!hasAlreadyCoSigned) {
trackEvent({
eventName: Name.REMIX_COSIGN,
properties: {
id: trackId,
handle: currentUser?.handle,
original_track_id: parentTrack?.track_id,
original_track_title: parentTrack?.title,
action: 'favorited'
}
})
}
}

// Dispatch the saveTrackSucceeded action
dispatch(tracksSocialActions.saveTrackSucceeded(trackId))
},
onError: (error, { trackId }, context) => {
if (!context) return

// Revert optimistic updates
queryClient.setQueryData(getTrackQueryKey(trackId), context.previousTrack)
dispatch(accountActions.decrementTrackSaveCount())

reportToSentry({
error,
additionalInfo: { trackId },
name: 'Favorite Track',
feature: Feature.Social
})
}
})
}
12 changes: 9 additions & 3 deletions packages/common/src/api/tan-query/useNotifications.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Id } from '@audius/sdk'
import { useInfiniteQuery } from '@tanstack/react-query'
import { InfiniteData, QueryKey, useInfiniteQuery } from '@tanstack/react-query'

import { notificationFromSDK, transformAndCleanList } from '~/adapters'
import { useAudiusQueryContext } from '~/audius-query/AudiusQueryContext'
Expand All @@ -23,7 +23,7 @@ const USER_INITIAL_LOAD_COUNT = 9

type PageParam = {
timestamp: number
groupId: string
groupId: string | undefined
} | null

type EntityIds = {
Expand Down Expand Up @@ -171,7 +171,13 @@ export const useNotifications = (options?: QueryOptions) => {
const validTypes = useNotificationValidTypes()
const pageSize = DEFAULT_LIMIT

const query = useInfiniteQuery({
const query = useInfiniteQuery<
Notification[],
Error,
InfiniteData<Notification[]>,
QueryKey,
PageParam
>({
queryKey: getNotificationsQueryKey({
currentUserId,
pageSize
Expand Down
6 changes: 3 additions & 3 deletions packages/common/src/api/tan-query/useSearchAutocomplete.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { OptionalId } from '@audius/sdk'
import { useQuery } from '@tanstack/react-query'
import { QueryKey, useQuery } from '@tanstack/react-query'

import { searchResultsFromSDK } from '~/adapters/search'
import { SearchResults, searchResultsFromSDK } from '~/adapters/search'
import { useAudiusQueryContext } from '~/audius-query'
import { useFeatureFlag } from '~/hooks/useFeatureFlag'
import { FeatureFlags } from '~/services/remote-config'
Expand Down Expand Up @@ -32,7 +32,7 @@ export const useSearchAutocomplete = (
FeatureFlags.USDC_PURCHASES
)

return useQuery({
return useQuery<SearchResults, Error, SearchResults, QueryKey>({
queryKey: getSearchAutocompleteQueryKey({ query, limit }),
queryFn: async () => {
const sdk = await audiusSdk()
Expand Down
14 changes: 12 additions & 2 deletions packages/common/src/api/tan-query/useSupportedUsers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { Id, OptionalId } from '@audius/sdk'
import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query'
import {
QueryKey,
useInfiniteQuery,
useQueryClient
} from '@tanstack/react-query'
import { useDispatch } from 'react-redux'

import { useAudiusQueryContext } from '~/audius-query'
Expand Down Expand Up @@ -35,7 +39,13 @@ export const useSupportedUsers = (
const { data: currentUserId } = useCurrentUserId()
const dispatch = useDispatch()

return useInfiniteQuery({
return useInfiniteQuery<
SupportedUserMetadata[],
Error,
SupportedUserMetadata[],
QueryKey,
number
>({
queryKey: getSupportedUsersQueryKey(userId, pageSize),
initialPageParam: 0,
getNextPageParam: (
Expand Down
26 changes: 21 additions & 5 deletions packages/common/src/api/tan-query/useSupporters.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import { Id, OptionalId } from '@audius/sdk'
import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query'
import {
QueryKey,
useInfiniteQuery,
useQueryClient
} from '@tanstack/react-query'
import { useDispatch } from 'react-redux'

import { useAudiusQueryContext } from '~/audius-query'
import { ID } from '~/models/Identifiers'
import { supporterMetadataListFromSDK } from '~/models/Tipping'
import {
SupporterMetadata,
supporterMetadataListFromSDK
} from '~/models/Tipping'

import { QUERY_KEYS } from './queryKeys'
import { QueryOptions } from './types'
Expand Down Expand Up @@ -33,14 +40,23 @@ export const useSupporters = (
const queryClient = useQueryClient()
const dispatch = useDispatch()

return useInfiniteQuery({
return useInfiniteQuery<
SupporterMetadata[],
Error,
SupporterMetadata[],
QueryKey,
number
>({
queryKey: getSupportersQueryKey(userId, pageSize),
initialPageParam: 0,
getNextPageParam: (lastPage, allPages) => {
getNextPageParam: (
lastPage: SupporterMetadata[],
allPages: SupporterMetadata[][]
) => {
if (lastPage.length < pageSize) return undefined
return allPages.length * pageSize
},
queryFn: async ({ pageParam }) => {
queryFn: async ({ pageParam }): Promise<SupporterMetadata[]> => {
const sdk = await audiusSdk()
const { data } = await sdk.full.users.getSupporters({
id: Id.parse(userId),
Expand Down
35 changes: 35 additions & 0 deletions packages/common/src/api/tan-query/useToggleFavoriteTrack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { useCallback } from 'react'

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

import { useFavoriteTrack } from './useFavoriteTrack'
import { useTrack } from './useTrack'
import { useUnfavoriteTrack } from './useUnfavoriteTrack'

type ToggleFavoriteTrackArgs = {
trackId: ID | null | undefined
source: string
}

export const useToggleFavoriteTrack = ({
trackId,
source
}: ToggleFavoriteTrackArgs) => {
const { mutate: favoriteTrack } = useFavoriteTrack()
const { mutate: unfavoriteTrack } = useUnfavoriteTrack()

const { data: isSaved } = useTrack(trackId, {
select: (track) => track?.has_current_user_saved
})

return useCallback(() => {
if (!trackId) {
return
}
if (isSaved) {
unfavoriteTrack({ trackId, source })
} else {
favoriteTrack({ trackId, source })
}
}, [isSaved, favoriteTrack, unfavoriteTrack, trackId, source])
}
4 changes: 2 additions & 2 deletions packages/common/src/api/tan-query/useTopTags.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Id } from '@audius/sdk'
import { useQuery } from '@tanstack/react-query'
import { QueryKey, useQuery } from '@tanstack/react-query'

import { useAudiusQueryContext } from '~/audius-query'
import { ID } from '~/models/Identifiers'
Expand All @@ -23,7 +23,7 @@ export const useTopTags = (
) => {
const { audiusSdk } = useAudiusQueryContext()

return useQuery({
return useQuery<string[], Error, string[], QueryKey>({
queryKey: getTopTagsQueryKey(userId),
queryFn: async () => {
try {
Expand Down
14 changes: 12 additions & 2 deletions packages/common/src/api/tan-query/useTrackHistory.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { Id, full } from '@audius/sdk'
import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query'
import {
QueryKey,
useInfiniteQuery,
useQueryClient
} from '@tanstack/react-query'
import { useDispatch } from 'react-redux'

import { trackActivityFromSDK, transformAndCleanList } from '~/adapters'
Expand Down Expand Up @@ -50,7 +54,13 @@ export const useTrackHistory = (
const queryClient = useQueryClient()
const dispatch = useDispatch()

const queryData = useInfiniteQuery({
const queryData = useInfiniteQuery<
UserTrackMetadata[],
Error,
UserTrackMetadata[],
QueryKey,
number
>({
initialPageParam: 0,
getNextPageParam: (lastPage: UserTrackMetadata[], allPages) => {
if (lastPage.length < pageSize) return undefined
Expand Down
Loading

0 comments on commit f4ee993

Please sign in to comment.