Skip to content

Commit

Permalink
[PAY-919][PAY-1312] Chat track and playlist tiles on mobile (#3455)
Browse files Browse the repository at this point in the history
Co-authored-by: Saliou Diallo <saliou@audius.co>
  • Loading branch information
sddioulde and Saliou Diallo authored Jun 7, 2023
1 parent e2896fe commit e6af9d7
Show file tree
Hide file tree
Showing 32 changed files with 830 additions and 298 deletions.
11 changes: 9 additions & 2 deletions packages/common/src/api/collection.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { ID, Kind } from 'models'
import { createApi } from 'src/audius-query/createApi'
import { Nullable } from 'utils'

const collectionApi = createApi({
reducerPath: 'collectionApi',
endpoints: {
getPlaylistById: {
fetch: async (
{ playlistId, currentUserId }: { playlistId: ID; currentUserId: ID },
{
playlistId,
currentUserId
}: { playlistId: ID; currentUserId: Nullable<ID> },
{ apiClient }
) => {
return (
Expand All @@ -25,7 +29,10 @@ const collectionApi = createApi({
// Note: Please do not use this endpoint yet as it depends on further changes on the DN side.
getPlaylistByPermalink: {
fetch: async (
{ permalink, currentUserId }: { permalink: string; currentUserId: ID },
{
permalink,
currentUserId
}: { permalink: string; currentUserId: Nullable<ID> },
{ apiClient }
) => {
return (
Expand Down
12 changes: 10 additions & 2 deletions packages/common/src/api/track.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ID, Kind } from 'models'
import { createApi } from 'src/audius-query/createApi'
import { parseTrackRouteFromPermalink } from 'utils/stringUtils'
import { Nullable } from 'utils/typeUtils'

const trackApi = createApi({
reducerPath: 'trackApi',
Expand All @@ -17,9 +18,16 @@ const trackApi = createApi({
},
getTrackByPermalink: {
fetch: async (
{ permalink, currentUserId }: { permalink: string; currentUserId: ID },
{
permalink,
currentUserId
}: { permalink: Nullable<string>; currentUserId: Nullable<ID> },
{ apiClient }
) => {
if (!permalink) {
console.error('Attempting to get track but permalink is null...')
return
}
const { handle, slug } = parseTrackRouteFromPermalink(permalink)
return await apiClient.getTrackByHandleAndSlug({
handle,
Expand All @@ -35,7 +43,7 @@ const trackApi = createApi({
},
getTracksByIds: {
fetch: async (
{ ids, currentUserId }: { ids: ID[]; currentUserId: ID },
{ ids, currentUserId }: { ids: ID[]; currentUserId: Nullable<ID> },
{ apiClient }
) => {
return await apiClient.getTracks({ ids, currentUserId })
Expand Down
2 changes: 2 additions & 0 deletions packages/common/src/hooks/chats/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export * from './types'
export * from './useCanSendMessage'
export * from './useSetInboxPermissions'
export * from './useTrackPlayer'
3 changes: 3 additions & 0 deletions packages/common/src/hooks/chats/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { Name } from 'models'

export type TrackPlayback = Name.PLAYBACK_PLAY | Name.PLAYBACK_PAUSE
134 changes: 134 additions & 0 deletions packages/common/src/hooks/chats/useTrackPlayer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { useCallback } from 'react'

import { useDispatch, useSelector } from 'react-redux'

import { ID, Name } from 'models'
import { getPlaying, getUid } from 'store/player/selectors'
import { QueueSource, Queueable, queueActions } from 'store/queue'
import { makeGetCurrent } from 'store/queue/selectors'
import { Nullable } from 'utils'

import { TrackPlayback } from './types'

const { clear, add, play, pause } = queueActions

type RecordAnalytics = ({ name, id }: { name: TrackPlayback; id: ID }) => void

type UseToggleTrack = {
uid: Nullable<string>
source: QueueSource
recordAnalytics?: RecordAnalytics
id?: Nullable<ID>
}

/**
* Hook that exposes a function to play a track.
* Optionally records a track play analytics event.
*
* @param {Function} recordAnalytics Function that tracks play event
*
* @returns {Function} the function that plays the track
*/
export const usePlayTrack = (recordAnalytics?: RecordAnalytics) => {
const dispatch = useDispatch()
const playingUid = useSelector(getUid)

const playTrack = useCallback(
({ id, uid, entries }: { id?: ID; uid: string; entries: Queueable[] }) => {
if (playingUid !== uid) {
dispatch(clear({}))
dispatch(add({ entries }))
dispatch(play({ uid }))
} else {
dispatch(play({}))
}
if (recordAnalytics && id) {
recordAnalytics({ name: Name.PLAYBACK_PLAY, id })
}
},
[dispatch, recordAnalytics, playingUid]
)

return playTrack
}

/**
* Hook that exposes a function to pause a track.
* Optionally records a track pause analytics event.
*
* @param {Function} recordAnalytics Function that tracks pause event
*
* @returns {Function} the function that pauses the track
*/
export const usePauseTrack = (recordAnalytics?: RecordAnalytics) => {
const dispatch = useDispatch()

const pauseTrack = useCallback(
(id?: ID) => {
dispatch(pause({}))
if (recordAnalytics && id) {
recordAnalytics({ name: Name.PLAYBACK_PAUSE, id })
}
},
[dispatch, recordAnalytics]
)

return pauseTrack
}

/**
* Represents that props passed into the useToggleTrack hook.
*
* @typedef {Object} UseToggleTrackProps
* @property {string} uid the uid of the track (nullable)
* @property {string} source the queue source
* @property {Function} recordAnalytics the function that tracks the event
* @property {number} id the id of the track (nullable and optional)
*/

/**
* Represents that props passed into the useToggleTrack hook.
*
* @typedef {Object} UseToggleTrackResult
* @property {Function} togglePlay the function that toggles the track i.e. play/pause
* @property {boolean} isTrackPlaying whether the track is playing or paused
*/

/**
* Hook that exposes a togglePlay function and isTrackPlaying boolean
* to facilitate the playing / pausing of a track.
* Also records the play / pause action as an analytics event.
* Leverages the useTrackPlay and useTrackPause hooks.
*
* @param {UseToggleTrackProps} param Object passed into function
*
* @returns {UseToggleTrackResult} the object that contains togglePlay and isTrackPlaying
*/
export const useToggleTrack = ({
uid,
source,
recordAnalytics,
id
}: UseToggleTrack) => {
const currentQueueItem = useSelector(makeGetCurrent())
const playing = useSelector(getPlaying)
const isTrackPlaying = !!(
playing &&
currentQueueItem.track &&
currentQueueItem.uid === uid
)

const playTrack = usePlayTrack(recordAnalytics)
const pauseTrack = usePauseTrack(recordAnalytics)

const togglePlay = useCallback(() => {
if (!id || !uid) return
if (isTrackPlaying) {
pauseTrack(id)
} else {
playTrack({ id, uid, entries: [{ id, uid, source }] })
}
}, [playTrack, pauseTrack, isTrackPlaying, id, uid, source])

return { togglePlay, isTrackPlaying }
}
8 changes: 8 additions & 0 deletions packages/common/src/store/pages/chat/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,11 @@ export enum ChatPermissionAction {
/** User is signed out and needs to sign in */
SIGN_UP
}

export type ChatMessageTileProps = {
link: string
styles?: any
onEmpty?: () => void
onSuccess?: () => void
className?: string
}
13 changes: 12 additions & 1 deletion packages/common/src/utils/stringUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,20 @@ export const paramsToQueryString = (params: {
}

/**
* Permalinks have the following format: '/<handle>/<track-slug>'
* Track permalinks have the following format: '/<handle>/<track-slug>'
*/
export const parseTrackRouteFromPermalink = (permalink: string) => {
const [, handle, slug] = permalink.split('/')
return { slug, trackId: null, handle }
}

/**
* Playlist permalinks have the following format: '/<handle>/playlist/<playlist-slug-with-id-at-the-end>'
*
* @param permalink
* @returns playlist id
*/
export const parsePlaylistIdFromPermalink = (permalink: string) => {
const playlistNameWithId = permalink?.split('/').slice(-1)[0] ?? ''
return parseInt(playlistNameWithId.split('-').slice(-1)[0])
}
16 changes: 11 additions & 5 deletions packages/mobile/src/components/lineup-tile/CollectionTile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,20 @@ const { getCollection, getTracksFromCollection } = cacheCollectionsSelectors
const getUserId = accountSelectors.getUserId

export const CollectionTile = (props: LineupItemProps) => {
const { uid } = props
const { uid, collection: collectionOverride, tracks: tracksOverride } = props

const collection = useProxySelector(
(state) => getCollection(state, { uid }),
[uid]
(state) => {
return collectionOverride ?? getCollection(state, { uid })
},
[collectionOverride, uid]
)

const tracks = useProxySelector(
(state) => getTracksFromCollection(state, { uid }),
[uid]
(state) => {
return tracksOverride ?? getTracksFromCollection(state, { uid })
},
[tracksOverride, uid]
)

const user = useProxySelector(
Expand Down Expand Up @@ -102,6 +106,7 @@ const CollectionTileComponent = ({
togglePlay,
tracks,
user,
variant,
...lineupTileProps
}: CollectionTileProps) => {
const dispatch = useDispatch()
Expand Down Expand Up @@ -253,6 +258,7 @@ const CollectionTileComponent = ({
title={playlist_name}
item={collection}
user={user}
variant={variant}
>
<CollectionTileTrackList tracks={tracks} onPress={handlePressTitle} />
</LineupTile>
Expand Down
35 changes: 21 additions & 14 deletions packages/mobile/src/components/lineup-tile/LineupTile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ export const LineupTile = ({
item,
user,
isPlayingUid,
variant,
styles,
TileProps
}: LineupTileProps) => {
const isGatedContentEnabled = useIsGatedContentEnabled()
Expand Down Expand Up @@ -99,8 +101,10 @@ export const LineupTile = ({
isTrack &&
(item.genre === Genre.PODCASTS || item.genre === Genre.AUDIOBOOKS)

const isReadonly = variant === 'readonly'

return (
<LineupTileRoot onPress={handlePress} {...TileProps}>
<LineupTileRoot onPress={handlePress} style={styles} {...TileProps}>
{showPremiumCornerTag && cornerTagIconType ? (
<LineupTileBannerIcon
type={cornerTagIconType}
Expand Down Expand Up @@ -143,6 +147,7 @@ export const LineupTile = ({
index={index}
isCollection={isCollection}
isTrending={isTrending}
variant={variant}
isUnlisted={isUnlisted}
playCount={playCount}
repostCount={repost_count}
Expand All @@ -151,19 +156,21 @@ export const LineupTile = ({
/>
</View>
{children}
<LineupTileActionButtons
hasReposted={has_current_user_reposted}
hasSaved={has_current_user_saved}
isOwner={isOwner}
isShareHidden={hideShare}
isUnlisted={isUnlisted}
trackId={trackId}
doesUserHaveAccess={doesUserHaveAccess}
onPressOverflow={onPressOverflow}
onPressRepost={onPressRepost}
onPressSave={onPressSave}
onPressShare={onPressShare}
/>
{!isReadonly ? (
<LineupTileActionButtons
hasReposted={has_current_user_reposted}
hasSaved={has_current_user_saved}
isOwner={isOwner}
isShareHidden={hideShare}
isUnlisted={isUnlisted}
trackId={trackId}
doesUserHaveAccess={doesUserHaveAccess}
onPressOverflow={onPressOverflow}
onPressRepost={onPressRepost}
onPressSave={onPressSave}
onPressShare={onPressShare}
/>
) : null}
</LineupTileRoot>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ const styles = StyleSheet.create({
type LineupTileRootProps = TileProps

export const LineupTileRoot = (props: LineupTileRootProps) => {
return <Tile {...props} styles={{ tile: styles.tile }} />
return <Tile {...props} styles={{ tile: [styles.tile, props.style] }} />
}
Loading

0 comments on commit e6af9d7

Please sign in to comment.