From 5962811e200d1f5330687bb5c5e527b8718f6117 Mon Sep 17 00:00:00 2001 From: Dylan Audius Date: Tue, 2 Jun 2026 15:29:46 -0700 Subject: [PATCH] feat(mobile): playing icon + skip-deleted-on-tap + gray deleted index MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two adjacent CollectionTile-in-lineup behaviors per design feedback: 1. **Speaker icon next to the title when actively playing.** Mirrors what TrackTile shows via LineupTileMetadata: a small IconVolumeLevel2 fills the line next to the title text while one of this collection's tracks is the currently-playing track AND the audio engine is actually playing (not paused). Same icon, same fill color (`useThemeColors().primary`), same `size='m'`. New `isPlaying = useSelector((state) => getPlaying(state) && isActive)` matches the derivation in LineupTileMetadata exactly. Restructured `styles.titleTouchable` to be a row container (`flexDirection: 'row', alignItems: 'center'`) and added `styles.titleText` (`flexShrink: 1`) so the title text shrinks to make room for the icon instead of pushing it off the tile. 2. **Don't try to play a deleted-by-artist track.** Previously `handlePress` fell back to `tracks[0]?.track_id` as the start track — if the first track had been deleted, the tile tap dispatched playback for an unplayable track. Now picks the current track only if it's one of ours AND not deleted, else the first non-deleted track; if every track in the collection is deleted, the tile tap is a no-op. 3. **Gray the index column on deleted rows.** `CollectionTileTrackList`'s `TrackItem` already grays the title, the "by " line, and the "[Deleted by Artist]" marker. The number column was still rendered in the regular subdued color, which read as "row partially deleted." Added `deleted && styles.deleted` to the index Text's style array so the whole row consistently grays out. The deleted-row tap itself was already a no-op for playback — the list-level Pressable only fires `handlePressTitle` (navigate to collection page). The real playability concern was at the tile-level tap handler, which (1) now covers. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) --- .../components/lineup-tile/CollectionTile.tsx | 47 +++++++++++++++++-- .../lineup-tile/CollectionTileTrackList.tsx | 13 ++++- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/packages/mobile/src/components/lineup-tile/CollectionTile.tsx b/packages/mobile/src/components/lineup-tile/CollectionTile.tsx index f3210188047..96c7b25bf41 100644 --- a/packages/mobile/src/components/lineup-tile/CollectionTile.tsx +++ b/packages/mobile/src/components/lineup-tile/CollectionTile.tsx @@ -29,12 +29,19 @@ import { formatLineupTileDuration, removeNullable } from '@audius/common/utils' import { TouchableOpacity, View } from 'react-native' import { useDispatch, useSelector } from 'react-redux' -import { Flex, Paper, Text, type ImageProps } from '@audius/harmony-native' +import { + Flex, + IconVolumeLevel2, + 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 { useThemeColors } from 'app/utils/theme' import { CollectionDogEar } from '../collection/CollectionDogEar' import { CollectionImage } from '../image/CollectionImage' @@ -45,7 +52,7 @@ import { LineupTileActionButtons } from './LineupTileActionButtons' import { TilePressBlockContext } from './TilePressBlockContext' import { LineupTileSource, type CollectionTileProps } from './types' -const { getTrackId } = playbackSelectors +const { getTrackId, getPlaying } = playbackSelectors const { requestOpen: requestOpenShareModal } = shareModalUIActions const { open: openOverflowMenu } = mobileOverflowMenuUIActions const { @@ -78,7 +85,15 @@ const useStyles = makeStyles(({ spacing }) => ({ gap: spacing(1) }, titleTouchable: { - width: '100%' + width: '100%', + flexDirection: 'row', + alignItems: 'center' + }, + titleText: { + flexShrink: 1 + }, + playingIndicator: { + marginLeft: 8 }, artistTouchable: { alignSelf: 'flex-start' @@ -104,6 +119,7 @@ export const CollectionTile = (props: CollectionTileProps) => { const dispatch = useDispatch() const navigation = useNavigation() const styles = useStyles() + const { primary } = useThemeColors() const { data: currentUserId } = useCurrentUserId() // Mirror the web mobile CollectionTile path exactly: fetch the collection @@ -135,6 +151,10 @@ export const CollectionTile = (props: CollectionTileProps) => { // already runs its own `getTrackId(state) === trackId` selector and // applies `styles.active` / `palette.primary` to its title text). const isActive = currentTrack != null + // True only while audio is actively playing (not paused) AND one of this + // collection's tracks is the current track. Mirrors LineupTileMetadata's + // `isPlaying` derivation — drives the speaker icon next to the title. + const isPlaying = useSelector((state) => getPlaying(state) && isActive) const isCollectionMarkedForDownload = useSelector((state) => collection @@ -165,7 +185,14 @@ export const CollectionTile = (props: CollectionTileProps) => { childPressedRef.current = false return } - const startTrackId = currentTrack?.track_id ?? tracks[0]?.track_id + // Don't try to play a deleted-by-artist track. Pick the current + // track (if it's one of ours and not deleted), else the first + // non-deleted track in the collection. If everything is deleted, + // the tile tap is a no-op. + const startTrackId = + currentTrack && !currentTrack.is_delete + ? currentTrack.track_id + : tracks.find((t) => !t.is_delete)?.track_id if (!startTrackId) return togglePlay({ id: startTrackId, @@ -312,9 +339,21 @@ export const CollectionTile = (props: CollectionTileProps) => { variant='title' color={isActive ? 'active' : 'default'} numberOfLines={1} + style={styles.titleText} > {collection.playlist_name} + {/* Speaker icon next to the title while one of this + collection's tracks is the currently-playing track and + the audio engine is actively playing (not paused). Same + icon + sizing TrackTile uses via LineupTileMetadata. */} + {isPlaying ? ( + + ) : null} { ) : !track ? null : ( <> - + {/* Index also picks up the deleted style so the whole row + grays out consistently — without this, the number column + stays the default subdued color while the title/artist + go further-subdued, which reads as "row partially + deleted" rather than the intended unavailable state. */} + {index + 1} {/*