Skip to content

Commit

Permalink
[C-3371] FollowArtistTile Play Track Preview (#6769)
Browse files Browse the repository at this point in the history
Co-authored-by: JD Francis <jd@audius.co>
  • Loading branch information
amendelsohn and DejayJD authored Nov 23, 2023
1 parent fdc7966 commit 482e382
Show file tree
Hide file tree
Showing 6 changed files with 231 additions and 28 deletions.
28 changes: 26 additions & 2 deletions packages/common/src/api/track.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,35 @@ const trackApi = createApi({
kind: Kind.TRACKS,
schemaKey: 'tracks'
}
},
getUserTracksByHandle: {
fetch: async (
{
handle,
currentUserId
}: { handle: string; currentUserId: Nullable<ID> },
{ apiClient }
) => {
return await apiClient.getUserTracksByHandle({
handle,
currentUserId,
getUnlisted: false
})
},
options: {
idListArgKey: 'ids',
kind: Kind.TRACKS,
schemaKey: 'tracks'
}
}
}
})

export const { useGetTrackById, useGetTrackByPermalink, useGetTracksByIds } =
trackApi.hooks
export const {
useGetTrackById,
useGetTrackByPermalink,
useGetTracksByIds,
useGetUserTracksByHandle
} = trackApi.hooks
export const trackApiFetch = trackApi.fetch
export const trackApiReducer = trackApi.reducer
2 changes: 1 addition & 1 deletion packages/common/src/api/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const userApi = createApi({
endpoints: {
getUserById: {
fetch: async (
{ id, currentUserId }: { id: ID; currentUserId: ID },
{ id, currentUserId }: { id: ID; currentUserId: Nullable<ID> },
{ apiClient }
) => {
const apiUser = await apiClient.getUser({ userId: id, currentUserId })
Expand Down
10 changes: 8 additions & 2 deletions packages/web/src/components/avatar/Avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
imageProfilePicEmpty
} from '@audius/common'
import {
Box,
Avatar as HarmonyAvatar,
type AvatarProps as HarmonyAvatarProps
} from '@audius/harmony'
Expand All @@ -28,10 +29,11 @@ const messages = {

type AvatarProps = Omit<HarmonyAvatarProps, 'src'> & {
userId: Maybe<ID>
onClick?: () => void
}

export const Avatar = (props: AvatarProps) => {
const { userId, ...other } = props
const { userId, onClick, ...other } = props
const profileImage = useProfilePicture(
userId ?? null,
SquareSizes.SIZE_150_BY_150
Expand All @@ -52,7 +54,11 @@ export const Avatar = (props: AvatarProps) => {
return user?.user_id === currentUser?.user_id ? messages.your : user?.name
})

return (
return onClick ? (
<Box w='100%' h='100%' onClick={onClick}>
<HarmonyAvatar src={image} {...other} />
</Box>
) : (
<Link to={goTo} aria-label={`${messages.goTo} ${name} ${messages.profile}`}>
<HarmonyAvatar src={image} {...other} />
</Link>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { HTMLProps } from 'react'
import { HTMLProps, useContext } from 'react'

import { UserMetadata, WidthSizes } from '@audius/common'
import {
Expand All @@ -7,17 +7,22 @@ import {
Flex,
FollowButton,
IconNote,
IconPause,
IconPlay,
IconUser,
IconVerified,
Paper,
Text
} from '@audius/harmony'
import { useField } from 'formik'
import { useHover } from 'react-use'

import { Avatar } from 'components/avatar/Avatar'
import { useMedia } from 'hooks/useMedia'
import { useCoverPhoto } from 'hooks/useUserCoverPhoto'

import { SelectArtistsPreviewContext } from '../utils/selectArtistsPreviewContext'

type FollowArtistTileProps = {
user: UserMetadata
} & HTMLProps<HTMLInputElement>
Expand All @@ -30,6 +35,52 @@ const FollowArtistTile = (props: FollowArtistTileProps) => {
const coverPhoto = useCoverPhoto(user_id, WidthSizes.SIZE_640)
const [followField] = useField({ name: 'selectedArtists', type: 'checkbox' })

const {
togglePreview,
nowPlayingArtistId,
isPlaying: isPreviewPlaying
} = useContext(SelectArtistsPreviewContext)

const isPlaying = isPreviewPlaying && nowPlayingArtistId === user_id

const [avatar] = useHover((isHovered) => (
<Box w={72} h={72} css={{ position: 'absolute', top: 34 }}>
<Flex
h={74}
w={74}
justifyContent='center'
alignItems='center'
css={{
visibility: isHovered || isPlaying ? 'visible' : 'hidden',
pointerEvents: 'none',
position: 'absolute',
top: 0,
left: 0,
borderRadius: 100,
opacity: 0.75,
background:
'radial-gradient(50% 50% at 50% 50%, rgba(0, 0, 0, 0.50) 0%, rgba(0, 0, 0, 0.10) 100%)',
zIndex: 2
}}
>
{isPlaying ? (
<IconPause size='l' color='staticWhite' />
) : (
<Box pl='xs'>
<IconPlay size='l' color='staticWhite' />
</Box>
)}
</Flex>
<Avatar
variant='strong'
userId={user_id}
onClick={() => {
togglePreview(user_id)
}}
/>
</Box>
))

return (
<Paper
h={220}
Expand All @@ -38,10 +89,7 @@ const FollowArtistTile = (props: FollowArtistTileProps) => {
}}
>
<Flex w='100%' direction='column' alignItems='center'>
<Box w={72} h={72} css={{ position: 'absolute', top: 34 }}>
{/* TODO: play song preview on click */}
<Avatar variant='strong' userId={user_id} />
</Box>
{avatar}
<Box w='100%' h={68} css={{ backgroundImage: `url(${coverPhoto})` }} />
<Flex
direction='column'
Expand Down
43 changes: 25 additions & 18 deletions packages/web/src/pages/sign-up-page/pages/SelectArtistsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { TRENDING_PAGE } from 'utils/route'
import { AccountHeader } from '../components/AccountHeader'
import { ContinueFooter } from '../components/ContinueFooter'
import FollowArtistTile from '../components/FollowArtistTile'
import { SelectArtistsPreviewContextProvider } from '../utils/selectArtistsPreviewContext'

const messages = {
header: 'Follow At Least 3 Artists',
Expand Down Expand Up @@ -204,24 +205,30 @@ export const SelectArtistsPage = () => {
</Flex>
<Form>
<fieldset>
<Paper
css={{
background: 'var(--harmony-bg-default)',
boxShadow: 'none',
minHeight: 500
}}
pv='xl'
ph={isMobile ? 'l' : 'xl'}
gap={isMobile ? 's' : 'm'}
wrap='wrap'
>
{isLoading
? null
: artists?.map((user) => {
const { user_id: userId } = user
return <FollowArtistTile key={userId} user={user} />
})}
</Paper>
<SelectArtistsPreviewContextProvider>
<Paper
css={{
background: 'var(--harmony-bg-default)',
boxShadow: 'none',
minHeight: 500
}}
pv='xl'
ph={isMobile ? 'l' : 'xl'}
gap={isMobile ? 's' : 'm'}
wrap='wrap'
>
{isLoading
? null
: artists?.map((user) => {
return (
<FollowArtistTile
key={user.user_id}
user={user}
/>
)
})}
</Paper>
</SelectArtistsPreviewContextProvider>
</fieldset>
</Form>
</Flex>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { createContext, useCallback, useEffect, useState } from 'react'

import {
ID,
encodeHashId,
useGetUserById,
useGetUserTracksByHandle
} from '@audius/common'
import { useUnmount } from 'react-use'

import { audioPlayer } from 'services/audio-player'
import { apiClient } from 'services/audius-api-client'

type PreviewContextProps = {
isPlaying: boolean
nowPlayingArtistId: number
playPreview: (artistId: ID) => void
stopPreview: () => void
togglePreview: (artistId: ID) => void
}

export const SelectArtistsPreviewContext = createContext<PreviewContextProps>({
isPlaying: false,
nowPlayingArtistId: -1,
playPreview: () => {},
stopPreview: () => {},
togglePreview: () => {}
})

export const SelectArtistsPreviewContextProvider = (props: {
children: JSX.Element
}) => {
const [isPlaying, setIsPlaying] = useState(false)
const [nowPlayingArtistId, setNowPlayingArtistId] = useState<number>(-1)
const [trackId, setTrackId] = useState<string | null>(null)

const { data: artist } = useGetUserById({
id: nowPlayingArtistId,
currentUserId: null
})
const { data: artistTracks } = useGetUserTracksByHandle(
{
handle: artist?.handle,
currentUserId: null
},
{ disabled: !artist?.handle }
)

useEffect(() => {
const trackId = artistTracks?.find((track) => track.is_available)?.track_id
trackId && setTrackId(encodeHashId(trackId))
}, [artistTracks])

const togglePlayback = useCallback(() => {
if (audioPlayer.isPlaying()) {
audioPlayer.pause()
setIsPlaying(false)
} else {
audioPlayer.play()
setIsPlaying(true)
}
}, [])

const stopPreview = useCallback(() => {
audioPlayer.stop()
setNowPlayingArtistId(-1)
setTrackId(null)
setIsPlaying(false)
}, [])

const playPreview = useCallback((artistId: ID) => {
if (audioPlayer.isPlaying()) {
audioPlayer.stop()
}
setNowPlayingArtistId(artistId)
setIsPlaying(true)
}, [])

const togglePreview = useCallback(
(artistId: ID) => {
if (artistId === nowPlayingArtistId) {
togglePlayback()
} else {
audioPlayer.stop()
setIsPlaying(false)
setNowPlayingArtistId(artistId)
}
},
[nowPlayingArtistId, togglePlayback]
)

useEffect(() => {
if (!trackId) return
audioPlayer.load(
0,
stopPreview,
apiClient.makeUrl(`/tracks/${trackId}/stream`)
)
audioPlayer.play()
setIsPlaying(true)
}, [nowPlayingArtistId, stopPreview, trackId])

useUnmount(stopPreview)

return (
<SelectArtistsPreviewContext.Provider
value={{
isPlaying,
nowPlayingArtistId,
playPreview,
stopPreview,
togglePreview
}}
>
{props.children}
</SelectArtistsPreviewContext.Provider>
)
}

0 comments on commit 482e382

Please sign in to comment.