diff --git a/packages/mobile/src/components/lineup-tile/CollectionTile.tsx b/packages/mobile/src/components/lineup-tile/CollectionTile.tsx
index 122d5e325bc..6974f6eb621 100644
--- a/packages/mobile/src/components/lineup-tile/CollectionTile.tsx
+++ b/packages/mobile/src/components/lineup-tile/CollectionTile.tsx
@@ -1,7 +1,13 @@
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,
@@ -9,7 +15,6 @@ import {
PlaybackSource,
SquareSizes
} from '@audius/common/models'
-import type { Track } from '@audius/common/models'
import {
collectionsSocialActions,
mobileOverflowMenuUIActions,
@@ -20,13 +25,16 @@ 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'
@@ -34,10 +42,8 @@ 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
@@ -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,
@@ -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, {
@@ -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
@@ -114,7 +129,7 @@ export const CollectionTile = (props: CollectionTileProps) => {
(props: ImageProps) => (
),
@@ -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)
@@ -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
@@ -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
}
@@ -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 (
-
+
+ {/* Card-style header: large square artwork above title + meta */}
+
+ {renderImage({ style: styles.artwork })}
+
+
+
+
+ {contentType}
+
+
+
+
+ {collection.playlist_name}
+
+
+ {durationText ? (
+
+ {durationText}
+
+ ) : null}
+
+
+
+
+
+
+
+
{
onPress={handlePressTitle}
onPressWithPropagationBlock={handlePressWithPropagationBlock}
isAlbum={collection.is_album}
- trackCount={tracks.length}
+ trackCount={trackCount}
+ isLoading={tracksLoading}
/>
{isReadonly ? null : (
void
trackCount: number
- tracks: LineupTrack[]
+ tracks: CollectionTrack[]
isAlbum: boolean
}
@@ -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
@@ -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 (
<>
@@ -121,7 +132,7 @@ const TrackItem = (props: TrackItemProps) => {
]}
numberOfLines={1}
>
- {`${messages.by} ${track.user?.name}`}
+ {`${messages.by} ${trackOwnerName ?? ''}`}
) : null}
{deleted ? (
diff --git a/packages/mobile/src/components/lineup/TrackLineup.tsx b/packages/mobile/src/components/lineup/TrackLineup.tsx
index 60f6092930c..9fe4ece185a 100644
--- a/packages/mobile/src/components/lineup/TrackLineup.tsx
+++ b/packages/mobile/src/components/lineup/TrackLineup.tsx
@@ -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.