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.