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
148 changes: 102 additions & 46 deletions packages/mobile/src/components/lineup-tile/CollectionTile.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import { useCallback, useMemo, useRef } from 'react'

import { useCollection, useCurrentUserId, useUser } from '@audius/common/api'
import {
useCollection,
useCurrentUserId,
useOrderedCollectionTracks,
useUser
} from '@audius/common/api'
import { useGatedCollectionAccess } from '@audius/common/hooks'
import type { CollectionTrack } from '@audius/common/api'
import {
ShareSource,
RepostSource,
FavoriteSource,
PlaybackSource,
SquareSizes
} from '@audius/common/models'
import type { Track } from '@audius/common/models'
import {
collectionsSocialActions,
mobileOverflowMenuUIActions,
Expand All @@ -20,24 +25,25 @@ import {
PurchaseableContentType
} from '@audius/common/store'
import type { CommonState } from '@audius/common/store'
import { removeNullable } from '@audius/common/utils'
import { formatLineupTileDuration, removeNullable } from '@audius/common/utils'
import { TouchableOpacity, View } from 'react-native'
import { useDispatch, useSelector } from 'react-redux'

import { Paper, type ImageProps } from '@audius/harmony-native'
import { Flex, Paper, Text, type ImageProps } from '@audius/harmony-native'
import { UserLink } from 'app/components/user-link'
import { useNavigation } from 'app/hooks/useNavigation'
import { setVisibility } from 'app/store/drawers/slice'
import { getIsCollectionMarkedForDownload } from 'app/store/offline-downloads/selectors'
import { makeStyles } from 'app/styles'

import { CollectionDogEar } from '../collection/CollectionDogEar'
import { CollectionImage } from '../image/CollectionImage'

import { CollectionTileStats } from './CollectionTileStats'
import { CollectionTileTrackList } from './CollectionTileTrackList'
import { LineupTileActionButtons } from './LineupTileActionButtons'
import { LineupTileMetadata } from './LineupTileMetadata'
import { TilePressBlockContext } from './TilePressBlockContext'
import { LineupTileSource, type CollectionTileProps } from './types'
import { useEnhancedCollectionTracks } from './useEnhancedCollectionTracks'

const { getTrackId } = playbackSelectors
const { requestOpen: requestOpenShareModal } = shareModalUIActions
Expand All @@ -49,9 +55,31 @@ const {
unsaveCollection
} = collectionsSocialActions

const useStyles = makeStyles(({ spacing }) => ({
artworkWrapper: {
width: '100%',
aspectRatio: 1,
overflow: 'hidden'
},
artwork: {
width: '100%',
height: '100%'
},
metadata: {
paddingHorizontal: spacing(4),
paddingVertical: spacing(3),
gap: spacing(1)
},
titleTouchable: {
flex: 1
},
artistTouchable: {
alignSelf: 'flex-start'
}
}))

export const CollectionTile = (props: CollectionTileProps) => {
const {
uid,
id,
collection: collectionOverride,
tracks: tracksOverride,
Expand All @@ -64,24 +92,15 @@ export const CollectionTile = (props: CollectionTileProps) => {

const dispatch = useDispatch()
const navigation = useNavigation()
const styles = useStyles()
const { data: currentUserId } = useCurrentUserId()

const { data: cachedCollection } = useCollection(id, {
select: (collection) => ({
has_current_user_reposted: collection.has_current_user_reposted,
has_current_user_saved: collection.has_current_user_saved,
is_album: collection.is_album,
playlist_id: collection.playlist_id,
playlist_name: collection.playlist_name,
playlist_owner_id: collection.playlist_owner_id,
stream_conditions: collection.stream_conditions,
is_private: collection.is_private,
is_delete: collection.is_delete,
playlist_contents: collection.playlist_contents
})
})
// Mirror the web mobile CollectionTile path exactly: fetch the collection
// by ID (no select), feed it to useOrderedCollectionTracks. Returns plain
// CollectionTrack[] / TrackMetadata[] — no UID plumbing required.
const { data: cachedCollection } = useCollection(id)
const collection = collectionOverride ?? cachedCollection
const collectionTracks = useEnhancedCollectionTracks(uid ?? '')
const collectionTracks = useOrderedCollectionTracks(cachedCollection)
const tracks = tracksOverride ?? collectionTracks

const { data: user } = useUser(collection?.playlist_owner_id, {
Expand All @@ -97,10 +116,6 @@ export const CollectionTile = (props: CollectionTileProps) => {
const trackId = getTrackId(state)
return tracks.find((track) => track.track_id === trackId) ?? null
})
const isPlayingUid = useSelector((state: CommonState) => {
const trackId = getTrackId(state)
return tracks.some((track) => track.track_id === trackId)
})

const isCollectionMarkedForDownload = useSelector((state) =>
collection
Expand All @@ -114,7 +129,7 @@ export const CollectionTile = (props: CollectionTileProps) => {
(props: ImageProps) => (
<CollectionImage
collectionId={collection?.playlist_id ?? 0}
size={SquareSizes.SIZE_150_BY_150}
size={SquareSizes.SIZE_480_BY_480}
{...props}
/>
),
Expand All @@ -131,9 +146,10 @@ export const CollectionTile = (props: CollectionTileProps) => {
childPressedRef.current = false
return
}
const startTrackId = currentTrack?.track_id ?? tracks[0]?.track_id
if (!startTrackId) return
togglePlay({
uid: currentTrack?.uid ?? tracks[0]?.uid ?? null,
id: currentTrack?.track_id ?? tracks[0]?.track_id ?? null,
id: startTrackId,
source: PlaybackSource.PLAYLIST_TILE_TRACK
})
}, 100)
Expand All @@ -150,11 +166,21 @@ export const CollectionTile = (props: CollectionTileProps) => {

const duration = useMemo(() => {
return tracks.reduce(
(duration: number, track: Track) => duration + track.duration,
(duration: number, track: CollectionTrack) =>
duration + (track.duration ?? 0),
0
)
}, [tracks])

// Use track_count if present on the collection (canonical full count);
// fall back to the loaded tracks length. Drives the skeleton state on the
// track list while tracks are still being fetched.
const trackCount =
collection?.track_count ??
collection?.playlist_contents?.track_ids?.length ??
tracks.length
const tracksLoading = tracks.length === 0 && trackCount > 0

const handlePressOverflow = useCallback(() => {
if (!collection) return
const isOwner = collection.playlist_owner_id === currentUserId
Expand Down Expand Up @@ -224,9 +250,6 @@ export const CollectionTile = (props: CollectionTileProps) => {
}, [collection, dispatch])

if (!collection || !tracks || !user) {
console.warn(
'Collection, tracks, or user missing for CollectionTile, preventing render'
)
return null
}

Expand All @@ -237,23 +260,55 @@ export const CollectionTile = (props: CollectionTileProps) => {
const isOwner = collection.playlist_owner_id === currentUserId
const isReadonly = variant === 'readonly'
const contentType = collection.is_album ? 'album' : 'playlist'
const durationText =
duration > 0 ? formatLineupTileDuration(duration, false, true) : null

return (
<TilePressBlockContext.Provider value={handlePressWithPropagationBlock}>
<Paper onPress={handlePress} style={style}>
<CollectionDogEar collectionId={collection.playlist_id} hideUnlocked />
<LineupTileMetadata
renderImage={renderImage}
onPressTitle={handlePressTitle}
onPressWithPropagationBlock={handlePressWithPropagationBlock}
title={collection.playlist_name}
userId={user.user_id}
isPlayingUid={isPlayingUid}
type={contentType}
trackId={collection.playlist_id}
duration={duration}
isLongFormContent={false}
/>

{/* Card-style header: large square artwork above title + meta */}
<View style={styles.artworkWrapper}>
{renderImage({ style: styles.artwork })}
</View>

<Flex column style={styles.metadata}>
<Text
variant='label'
size='xs'
textTransform='uppercase'
color='subdued'
>
{contentType}
</Text>
<Flex row alignItems='center' justifyContent='space-between' gap='s'>
<TouchableOpacity
style={styles.titleTouchable}
onPressIn={handlePressWithPropagationBlock}
onPress={handlePressTitle}
>
<Text variant='title' strength='strong' numberOfLines={1}>
{collection.playlist_name}
</Text>
</TouchableOpacity>
{durationText ? (
<Text variant='body' size='s' color='subdued'>
{durationText}
</Text>
) : null}
</Flex>
<TouchableOpacity
onPressIn={handlePressWithPropagationBlock}
activeOpacity={0.7}
style={styles.artistTouchable}
>
<View pointerEvents='none'>
<UserLink textVariant='body' userId={user.user_id} />
</View>
</TouchableOpacity>
</Flex>

<CollectionTileStats
collectionId={collection.playlist_id}
rankIndex={lineupTileProps.index}
Expand All @@ -264,7 +319,8 @@ export const CollectionTile = (props: CollectionTileProps) => {
onPress={handlePressTitle}
onPressWithPropagationBlock={handlePressWithPropagationBlock}
isAlbum={collection.is_album}
trackCount={tracks.length}
trackCount={trackCount}
isLoading={tracksLoading}
/>
{isReadonly ? null : (
<LineupTileActionButtons
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import type { ID, LineupTrack } from '@audius/common/models'
import type { CollectionTrack } from '@audius/common/api'
import { useUser } from '@audius/common/api'
import type { ID } from '@audius/common/models'
import type { CommonState } from '@audius/common/store'
import { playbackSelectors } from '@audius/common/store'
import { pluralize } from '@audius/common/utils'
Expand All @@ -25,7 +27,7 @@ type LineupTileTrackListProps = {
onPress: GestureResponderHandler
onPressWithPropagationBlock?: () => void
trackCount: number
tracks: LineupTrack[]
tracks: CollectionTrack[]
isAlbum: boolean
}

Expand Down Expand Up @@ -77,7 +79,7 @@ const useStyles = makeStyles(({ palette, spacing, typography }) => ({
type TrackItemProps = {
showSkeleton?: boolean
index: number
track?: LineupTrack
track?: CollectionTrack
trackId?: ID
isAlbum?: boolean
deleted?: boolean
Expand All @@ -89,6 +91,15 @@ const TrackItem = (props: TrackItemProps) => {
const isPlayingUid = useSelector(
(state: CommonState) => getTrackId(state) === trackId
)
// Mirror the web mobile CollectionTile.TrackItem path exactly: tracks
// returned by `useOrderedCollectionTracks` are plain CollectionTrack /
// TrackMetadata (no joined `user`), so we fetch the owner's display name
// per row from the user cache. Hooks must run before any conditional
// returns; passing `undefined` to useUser when no track is present is
// safe (selector returns undefined).
const { data: trackOwnerName } = useUser(track?.owner_id, {
select: (user) => user?.name
})
return (
<>
<View style={styles.divider} />
Expand Down Expand Up @@ -121,7 +132,7 @@ const TrackItem = (props: TrackItemProps) => {
]}
numberOfLines={1}
>
{`${messages.by} ${track.user?.name}`}
{`${messages.by} ${trackOwnerName ?? ''}`}
</Text>
) : null}
{deleted ? (
Expand Down
1 change: 0 additions & 1 deletion packages/mobile/src/components/lineup/TrackLineup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,6 @@ export const TrackLineup = ({
(id: ID) => makeStableUid(Kind.TRACKS, id, source),
[source]
)

// Build a single ordered list of mixed track/collection entries. When the
// caller passes `lineupItems` (mixed feed) we use it verbatim; otherwise we
// wrap the legacy `trackIds` so the rest of the component is uniform.
Expand Down
Loading