-
Notifications
You must be signed in to change notification settings - Fork 113
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
- Loading branch information
1 parent
db6a857
commit f4ee993
Showing
40 changed files
with
688 additions
and
335 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}) | ||
} | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
35 changes: 35 additions & 0 deletions
35
packages/common/src/api/tan-query/useToggleFavoriteTrack.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.