From d91daff4d83fe6b272fb2bd2d215bde61c8f79f8 Mon Sep 17 00:00:00 2001 From: Dylan Audius Date: Thu, 22 Oct 2026 12:24:12 -0700 Subject: [PATCH] feat(mobile): dense track rows on catalog lineups behind track_rows Mobile catalog lineups render tracks as 64px rows instead of bordered tiles, matching web. Behind the existing track_rows flag; Feed keeps tiles. - harmony-native TrackRow: optional rank, 48px art, title, artist line, duration or a status (price/unlock), and a pressed state. Long-press opens the track's actions and is exposed to VoiceOver as an action. - TrackRowTile: connected row with the tile's play and gated-content behavior, an equalizer on the playing track's art, Artist Pick context, and podcast time-left labels. - useOpenTrackOverflow: overflow actions shared by TrackTile and the row. Rows add share, favorite and repost since they have no inline buttons. - TrackLineup tileVariant='row'; used on Trending, Underground, profile Tracks and Reposts, and search track results. Co-Authored-By: Claude Opus 5.5 --- .../components/lineup-tile/TrackRowTile.tsx | 253 ++++++++++++++++++ .../src/components/lineup-tile/TrackTile.tsx | 66 +---- .../src/components/lineup-tile/index.ts | 1 + .../lineup-tile/useOpenTrackOverflow.ts | 117 ++++++++ .../src/components/lineup/TrackLineup.tsx | 35 ++- .../components/TrackRow/TrackRow.tsx | 175 ++++++++++++ .../src/harmony-native/components/index.ts | 1 + .../profile-screen/ProfileTabs/RepostsTab.tsx | 6 + .../profile-screen/ProfileTabs/TracksTab.tsx | 6 + .../search-results/TrackResults.tsx | 6 + .../trending-screen/TrendingLineup.tsx | 6 + .../TrendingUndergroundLineup.tsx | 6 + 12 files changed, 616 insertions(+), 62 deletions(-) create mode 100644 packages/mobile/src/components/lineup-tile/TrackRowTile.tsx create mode 100644 packages/mobile/src/components/lineup-tile/useOpenTrackOverflow.ts create mode 100644 packages/mobile/src/harmony-native/components/TrackRow/TrackRow.tsx diff --git a/packages/mobile/src/components/lineup-tile/TrackRowTile.tsx b/packages/mobile/src/components/lineup-tile/TrackRowTile.tsx new file mode 100644 index 00000000000..6e3b0565a14 --- /dev/null +++ b/packages/mobile/src/components/lineup-tile/TrackRowTile.tsx @@ -0,0 +1,253 @@ +import { memo, useCallback, useContext } from 'react' + +import { useCurrentUserId, useTrack, useUser } from '@audius/common/api' +import { useGatedTrackAccess } from '@audius/common/hooks' +import { + ModalSource, + PlaybackSource, + SquareSizes, + isContentUSDCPurchaseGated +} from '@audius/common/models' +import { + PurchaseableContentType, + gatedContentActions, + playbackPositionSelectors, + playbackSelectors, + usePremiumContentPurchaseModal +} from '@audius/common/store' +import type { CommonState } from '@audius/common/store' +import { Genre, formatLineupTileDuration } from '@audius/common/utils' +import { View } from 'react-native' +import { useDispatch, useSelector } from 'react-redux' + +import { + Flex, + IconPin, + IconPlay, + IconText, + Text, + TrackRow +} from '@audius/harmony-native' +import { LineupContext } from 'app/components/lineup/LineupContext' +import type { TrackTileProps } from 'app/components/lineup-tile/types' +import { AnimatedEqBars } from 'app/components/track-list/AnimatedEqBars' +import { UserBadges } from 'app/components/user-badges' +import * as haptics from 'app/haptics' +import { useIsUSDCEnabled } from 'app/hooks/useIsUSDCEnabled' +import { setVisibility } from 'app/store/drawers/slice' + +import { TrackImage } from '../image/TrackImage' + +import { LineupTileAccessStatus } from './LineupTileAccessStatus' +import { useOpenTrackOverflow } from './useOpenTrackOverflow' + +const { getTrackId, getPlaying } = playbackSelectors +const { getTrackPosition } = playbackPositionSelectors +const { setLockedContentId } = gatedContentActions + +const messages = { + artistPick: 'Artist Pick', + timeLeft: 'left', + played: 'Played', + play: (title: string) => `Play ${title}`, + pause: (title: string) => `Pause ${title}` +} + +const overlayStyle = { + position: 'absolute' as const, + top: 0, + left: 0, + right: 0, + bottom: 0, + // Above Artwork's image, which sets zIndex 2 + zIndex: 3, + alignItems: 'center' as const, + backgroundColor: 'rgba(0,0,0,0.45)' +} + +const TrackRowTileComponent = (props: TrackTileProps) => { + const { + id, + uid, + index, + isTrending, + togglePlay, + onPress, + showArtistPick = false, + source + } = props + + const dispatch = useDispatch() + const { isOnArtistsTracksTab } = useContext(LineupContext) + const { data: currentUserId } = useCurrentUserId() + const { onOpen: openPremiumContentPurchaseModal } = + usePremiumContentPurchaseModal() + const isUSDCEnabled = useIsUSDCEnabled() + const { hasStreamAccess } = useGatedTrackAccess(id) + + const { data: track } = useTrack(id, { + select: (track) => ({ + track_id: track.track_id, + title: track.title, + owner_id: track.owner_id, + duration: track.duration, + genre: track.genre, + stream_conditions: track.stream_conditions, + preview_cid: track.preview_cid, + is_delete: track.is_delete + }) + }) + const { data: user } = useUser(track?.owner_id, { + select: (user) => ({ + name: user.name, + artist_pick_track_id: user.artist_pick_track_id, + is_deactivated: user.is_deactivated + }) + }) + + const isActive = useSelector((state: CommonState) => getTrackId(state) === id) + const isPlaying = useSelector( + (state: CommonState) => getTrackId(state) === id && getPlaying(state) + ) + const playbackPosition = useSelector((state) => + getTrackPosition(state, { trackId: id, userId: currentUserId }) + ) + + const handlePressOverflow = useOpenTrackOverflow(id, { + includeSocial: true, + hideViewArtist: isOnArtistsTracksTab + }) + + const handleLongPress = useCallback(() => { + haptics.medium() + handlePressOverflow() + }, [handlePressOverflow]) + + const handlePress = useCallback(() => { + if (!track) return + const isStreamGated = !!track.stream_conditions + if (isStreamGated && !hasStreamAccess && !track.preview_cid) { + if ( + isUSDCEnabled && + isContentUSDCPurchaseGated(track.stream_conditions) + ) { + openPremiumContentPurchaseModal( + { + contentId: track.track_id, + contentType: PurchaseableContentType.TRACK + }, + { source: ModalSource.LineUpTrackTile } + ) + } else { + dispatch(setLockedContentId({ id: track.track_id })) + dispatch(setVisibility({ drawer: 'LockedContent', visible: true })) + } + return + } + togglePlay({ uid, id: track.track_id, source: PlaybackSource.TRACK_TILE }) + onPress?.(track.track_id) + }, [ + track, + hasStreamAccess, + isUSDCEnabled, + openPremiumContentPurchaseModal, + dispatch, + togglePlay, + uid, + onPress + ]) + + const rank = isTrending && index !== undefined ? index + 1 : undefined + + if (!track || !user) return + if (track.is_delete || user.is_deactivated) return null + + const isOwner = currentUserId === track.owner_id + const isLocked = !!track.stream_conditions && !hasStreamAccess && !isOwner + const isLongForm = + track.genre === Genre.Podcasts || track.genre === Genre.Audiobooks + + const duration = !track.duration + ? undefined + : isLongForm && playbackPosition?.status === 'COMPLETED' + ? messages.played + : isLongForm && playbackPosition?.status === 'IN_PROGRESS' + ? `${formatLineupTileDuration( + track.duration - playbackPosition.playbackPosition, + true + )} ${messages.timeLeft}` + : formatLineupTileDuration(track.duration, isLongForm) + + const isArtistPick = + showArtistPick && user.artist_pick_track_id === track.track_id + + return ( + + + {isActive ? ( + + {isPlaying ? ( + + ) : ( + + )} + + ) : null} + + } + title={track.title} + subtitle={ + + + {user.name} + + + + } + context={ + isArtistPick ? ( + {messages.artistPick} + ) : undefined + } + status={ + isLocked && track.stream_conditions ? ( + + ) : undefined + } + duration={duration} + isActive={isActive} + onPress={handlePress} + onLongPress={handleLongPress} + accessibilityLabel={ + isPlaying ? messages.pause(track.title) : messages.play(track.title) + } + /> + ) +} + +export const TrackRowTile = memo(TrackRowTileComponent) diff --git a/packages/mobile/src/components/lineup-tile/TrackTile.tsx b/packages/mobile/src/components/lineup-tile/TrackTile.tsx index ae53d8830c1..0c3df259523 100644 --- a/packages/mobile/src/components/lineup-tile/TrackTile.tsx +++ b/packages/mobile/src/components/lineup-tile/TrackTile.tsx @@ -18,12 +18,8 @@ import { } from '@audius/common/models' import { tracksSocialActions, - mobileOverflowMenuUIActions, shareModalUIActions, - OverflowAction, - OverflowSource, playbackSelectors, - playbackPositionSelectors, trackPageActions, PurchaseableContentType, gatedContentActions, @@ -31,7 +27,7 @@ import { usePublishConfirmationModal } from '@audius/common/store' import type { CommonState } from '@audius/common/store' -import { Genre, removeNullable } from '@audius/common/utils' +import { Genre } from '@audius/common/utils' import { useDispatch, useSelector } from 'react-redux' import { Paper, type ImageProps } from '@audius/harmony-native' @@ -48,12 +44,11 @@ import { LineupTileActionButtons } from './LineupTileActionButtons' import { LineupTileMetadata } from './LineupTileMetadata' import { TilePressBlockContext } from './TilePressBlockContext' import { TrackTileStats } from './TrackTileStats' +import { useOpenTrackOverflow } from './useOpenTrackOverflow' const { getTrackId } = playbackSelectors const { requestOpen: requestOpenShareModal } = shareModalUIActions -const { open: openOverflowMenu } = mobileOverflowMenuUIActions const { repostTrack, undoRepostTrack } = tracksSocialActions -const { getTrackPosition } = playbackPositionSelectors const { setLockedContentId } = gatedContentActions const TrackTileComponent = (props: TrackTileProps) => { @@ -195,60 +190,9 @@ const TrackTileComponent = (props: TrackTileProps) => { childPressedRef.current = true }, []) - const playbackPositionInfo = useSelector((state) => - getTrackPosition(state, { - trackId: track?.track_id ?? 0, - userId: currentUserId - }) - ) - - const handlePressOverflow = useCallback(() => { - if (!track) return - const isLongFormContent = - track.genre === Genre.Podcasts || track.genre === Genre.Audiobooks - const isOwner = currentUserId === track.owner_id - const isArtistPick = - isOwner && user?.artist_pick_track_id === track.track_id - - const overflowActions = [ - !track.is_unlisted || isOwner ? OverflowAction.PLAY_NEXT : null, - !track.is_unlisted || isOwner ? OverflowAction.ADD_TO_QUEUE : null, - isOwner && !track.ddex_app ? OverflowAction.ADD_TO_ALBUM : null, - !track.is_unlisted || isOwner ? OverflowAction.ADD_TO_PLAYLIST : null, - isLongFormContent - ? OverflowAction.VIEW_EPISODE_PAGE - : OverflowAction.VIEW_TRACK_PAGE, - track.album_backlink ? OverflowAction.VIEW_ALBUM_PAGE : null, - isLongFormContent - ? playbackPositionInfo?.status === 'COMPLETED' - ? OverflowAction.MARK_AS_UNPLAYED - : OverflowAction.MARK_AS_PLAYED - : null, - isOwner && !isArtistPick ? OverflowAction.SET_ARTIST_PICK : null, - isArtistPick ? OverflowAction.UNSET_ARTIST_PICK : null, - isOnArtistsTracksTab ? null : OverflowAction.VIEW_ARTIST_PAGE, - isOwner && !track.ddex_app ? OverflowAction.EDIT_TRACK : null, - isOwner && track?.is_scheduled_release && track?.is_unlisted - ? OverflowAction.RELEASE_NOW - : null, - isOwner && !track.ddex_app ? OverflowAction.DELETE_TRACK : null - ].filter(removeNullable) - - dispatch( - openOverflowMenu({ - source: OverflowSource.TRACKS, - id: track.track_id, - overflowActions - }) - ) - }, [ - track, - currentUserId, - user?.artist_pick_track_id, - isOnArtistsTracksTab, - playbackPositionInfo?.status, - dispatch - ]) + const handlePressOverflow = useOpenTrackOverflow(id, { + hideViewArtist: isOnArtistsTracksTab + }) const handlePressShare = useCallback(() => { if (!track) return diff --git a/packages/mobile/src/components/lineup-tile/index.ts b/packages/mobile/src/components/lineup-tile/index.ts index 962a5ed7fb4..2c7cc4db87f 100644 --- a/packages/mobile/src/components/lineup-tile/index.ts +++ b/packages/mobile/src/components/lineup-tile/index.ts @@ -2,3 +2,4 @@ export { CollectionTile } from './CollectionTile' export { CollectionTileSkeleton } from './CollectionTileSkeleton' export { LineupTileSkeleton } from './LineupTileSkeleton' export { TrackTile } from './TrackTile' +export { TrackRowTile } from './TrackRowTile' diff --git a/packages/mobile/src/components/lineup-tile/useOpenTrackOverflow.ts b/packages/mobile/src/components/lineup-tile/useOpenTrackOverflow.ts new file mode 100644 index 00000000000..426d75f8f1e --- /dev/null +++ b/packages/mobile/src/components/lineup-tile/useOpenTrackOverflow.ts @@ -0,0 +1,117 @@ +import { useCallback } from 'react' + +import { useCurrentUserId, useTrack, useUser } from '@audius/common/api' +import { useGatedTrackAccess } from '@audius/common/hooks' +import type { ID } from '@audius/common/models' +import { + mobileOverflowMenuUIActions, + OverflowAction, + OverflowSource, + playbackPositionSelectors +} from '@audius/common/store' +import { Genre, removeNullable } from '@audius/common/utils' +import { useDispatch, useSelector } from 'react-redux' + +const { open: openOverflowMenu } = mobileOverflowMenuUIActions +const { getTrackPosition } = playbackPositionSelectors + +type Options = { + /** + * Adds share, favorite and repost. Rows have no inline buttons for these, + * so they go in the menu. + */ + includeSocial?: boolean + hideViewArtist?: boolean +} + +/** Opens the track overflow menu used by lineup tiles and rows. */ +export const useOpenTrackOverflow = (id: ID, options: Options = {}) => { + const { includeSocial = false, hideViewArtist = false } = options + const dispatch = useDispatch() + const { data: currentUserId } = useCurrentUserId() + const { hasStreamAccess } = useGatedTrackAccess(id) + + const { data: track } = useTrack(id, { + select: (track) => ({ + track_id: track.track_id, + owner_id: track.owner_id, + genre: track.genre, + is_unlisted: track.is_unlisted, + is_scheduled_release: track.is_scheduled_release, + ddex_app: track.ddex_app, + album_backlink: track.album_backlink, + has_current_user_saved: track.has_current_user_saved, + has_current_user_reposted: track.has_current_user_reposted + }) + }) + const { data: artistPickTrackId } = useUser(track?.owner_id, { + select: (user) => user.artist_pick_track_id + }) + const playbackStatus = useSelector( + (state) => + getTrackPosition(state, { trackId: id, userId: currentUserId })?.status + ) + + return useCallback(() => { + if (!track) return + const isLongFormContent = + track.genre === Genre.Podcasts || track.genre === Genre.Audiobooks + const isOwner = currentUserId === track.owner_id + const isArtistPick = isOwner && artistPickTrackId === track.track_id + const canShare = !track.is_unlisted || isOwner + const canEngage = + includeSocial && !isOwner && !track.is_unlisted && hasStreamAccess + + const overflowActions = [ + !track.is_unlisted || isOwner ? OverflowAction.PLAY_NEXT : null, + !track.is_unlisted || isOwner ? OverflowAction.ADD_TO_QUEUE : null, + includeSocial && canShare ? OverflowAction.SHARE : null, + canEngage + ? track.has_current_user_saved + ? OverflowAction.UNFAVORITE + : OverflowAction.FAVORITE + : null, + canEngage + ? track.has_current_user_reposted + ? OverflowAction.UNREPOST + : OverflowAction.REPOST + : null, + isOwner && !track.ddex_app ? OverflowAction.ADD_TO_ALBUM : null, + !track.is_unlisted || isOwner ? OverflowAction.ADD_TO_PLAYLIST : null, + isLongFormContent + ? OverflowAction.VIEW_EPISODE_PAGE + : OverflowAction.VIEW_TRACK_PAGE, + track.album_backlink ? OverflowAction.VIEW_ALBUM_PAGE : null, + isLongFormContent + ? playbackStatus === 'COMPLETED' + ? OverflowAction.MARK_AS_UNPLAYED + : OverflowAction.MARK_AS_PLAYED + : null, + isOwner && !isArtistPick ? OverflowAction.SET_ARTIST_PICK : null, + isArtistPick ? OverflowAction.UNSET_ARTIST_PICK : null, + hideViewArtist ? null : OverflowAction.VIEW_ARTIST_PAGE, + isOwner && !track.ddex_app ? OverflowAction.EDIT_TRACK : null, + isOwner && track.is_scheduled_release && track.is_unlisted + ? OverflowAction.RELEASE_NOW + : null, + isOwner && !track.ddex_app ? OverflowAction.DELETE_TRACK : null + ].filter(removeNullable) + + dispatch( + openOverflowMenu({ + source: OverflowSource.TRACKS, + id: track.track_id, + overflowActions + }) + ) + }, [ + track, + currentUserId, + artistPickTrackId, + includeSocial, + hasStreamAccess, + hideViewArtist, + playbackStatus, + dispatch + ]) +} diff --git a/packages/mobile/src/components/lineup/TrackLineup.tsx b/packages/mobile/src/components/lineup/TrackLineup.tsx index bebabeee22b..0e8c3fb7b1c 100644 --- a/packages/mobile/src/components/lineup/TrackLineup.tsx +++ b/packages/mobile/src/components/lineup/TrackLineup.tsx @@ -36,10 +36,12 @@ import type { import { StyleSheet, View } from 'react-native' import { useDispatch, useStore } from 'react-redux' +import { TrackRow } from '@audius/harmony-native' import { SectionList } from 'app/components/core' import { useBottomChinHeight } from 'app/components/core/BottomChin' import { TrackTile, + TrackRowTile, CollectionTile, LineupTileSkeleton } from 'app/components/lineup-tile' @@ -72,7 +74,10 @@ type Section = { const SkeletonTileView = memo(function SkeletonTileView(props: { itemStyles?: ViewStyle + isRow?: boolean + rank?: number }) { + if (props.isRow) return return ( @@ -113,6 +118,8 @@ export type TrackLineupProps = { initialPageSize?: number maxEntries?: number isTrending?: boolean + /** 'row' renders tracks as dense rows; collections stay tiles. */ + tileVariant?: 'tile' | 'row' rankIconCount?: number showArtistPick?: boolean header?: SectionListProps['ListHeaderComponent'] @@ -166,6 +173,7 @@ export const TrackLineup = ({ initialPageSize, maxEntries = Infinity, isTrending = false, + tileVariant = 'tile', rankIconCount = 0, showArtistPick = false, header, @@ -358,13 +366,37 @@ export const TrackLineup = ({ maxEntries ]) + const isRow = tileVariant === 'row' + const renderItem = useCallback( ({ item, index }: { item: RenderItem; index: number }) => { if ((item as LoadingItem)._loading) { - return + return ( + + ) } const entry = item as Entry const delineator = delineatorMap?.[index] ?? null + if (isRow && entry.kind === 'track') { + return ( + <> + + {delineator} + + ) + } return ( <> @@ -395,6 +427,7 @@ export const TrackLineup = ({ }, [ itemStyles, + isRow, isTrending, togglePlay, onPressItem, diff --git a/packages/mobile/src/harmony-native/components/TrackRow/TrackRow.tsx b/packages/mobile/src/harmony-native/components/TrackRow/TrackRow.tsx new file mode 100644 index 00000000000..d1fe608e99c --- /dev/null +++ b/packages/mobile/src/harmony-native/components/TrackRow/TrackRow.tsx @@ -0,0 +1,175 @@ +import type { ReactNode } from 'react' + +import { useTheme } from '@emotion/react' +import { Pressable, View } from 'react-native' + +import { Skeleton } from '../Skeleton' +import { Text } from '../Text/Text' + +export const TRACK_ROW_HEIGHT = 64 +const ARTWORK_SIZE = 48 +const RANK_WIDTH = 20 + +// Skeleton fills its parent absolutely by default; rows lay it out inline. +const skeletonStyle = { position: 'relative' as const, borderRadius: 4 } + +export type TrackRowProps = { + /** 1-based position shown before the artwork. Omit for unranked lists. */ + rank?: number + /** 48px square artwork, including any playing overlay. */ + artwork?: ReactNode + title?: string + /** Artist line under the title. */ + subtitle?: ReactNode + /** Shown in place of the duration, e.g. a price button on a locked track. */ + status?: ReactNode + /** Pre-formatted duration, e.g. "2:30". */ + duration?: string + /** One short line above the title, such as "Artist Pick". */ + context?: ReactNode + /** This track is the one loaded in the player. */ + isActive?: boolean + isLoading?: boolean + isDisabled?: boolean + onPress?: () => void + /** Opens the track's actions. Also exposed to screen readers as an action. */ + onLongPress?: () => void + /** Accessible name for the row's press action. */ + accessibilityLabel?: string +} + +export const TrackRow = (props: TrackRowProps) => { + const { + rank, + artwork, + title, + subtitle, + status, + duration, + context, + isActive, + isLoading, + isDisabled, + onPress, + onLongPress, + accessibilityLabel + } = props + const { color, spacing } = useTheme() + + const rankCell = + rank === undefined ? null : ( + + + {rank} + + + ) + const paddingLeft = rank === undefined ? spacing.l : spacing.m + + if (isLoading) { + return ( + + {rankCell} + + + + + + + ) + } + + return ( + { + if (event.nativeEvent.actionName === 'longpress') onLongPress?.() + }} + style={({ pressed }) => ({ + flexDirection: 'row', + alignItems: 'center', + gap: spacing.m, + minHeight: TRACK_ROW_HEIGHT, + paddingVertical: spacing.s, + paddingLeft, + paddingRight: spacing.l, + backgroundColor: pressed ? color.background.surface1 : undefined, + opacity: isDisabled ? 0.5 : 1 + })} + > + {rankCell} + + {artwork} + + + {context} + + {title} + + {subtitle} + + {status ?? + (duration ? ( + + {duration} + + ) : null)} + + ) +} diff --git a/packages/mobile/src/harmony-native/components/index.ts b/packages/mobile/src/harmony-native/components/index.ts index f82a532997b..ce2cc849d51 100644 --- a/packages/mobile/src/harmony-native/components/index.ts +++ b/packages/mobile/src/harmony-native/components/index.ts @@ -36,3 +36,4 @@ export * from './Skeleton' export * from './NotificationCount/NotificationCount' export * from './LoadingSpinner/LoadingSpinner' export * from './IconLogoWhiteBackground' +export * from './TrackRow/TrackRow' diff --git a/packages/mobile/src/screens/profile-screen/ProfileTabs/RepostsTab.tsx b/packages/mobile/src/screens/profile-screen/ProfileTabs/RepostsTab.tsx index 928f02b3280..f97cb4efb49 100644 --- a/packages/mobile/src/screens/profile-screen/ProfileTabs/RepostsTab.tsx +++ b/packages/mobile/src/screens/profile-screen/ProfileTabs/RepostsTab.tsx @@ -5,12 +5,17 @@ import { useProfileReposts, getProfileRepostsQueryKey } from '@audius/common/api' +import { useFeatureFlag } from '@audius/common/hooks' +import { FeatureFlags } from '@audius/common/services' import { TrackLineup } from 'app/components/lineup/TrackLineup' import { EmptyProfileTile } from '../EmptyProfileTile' export const RepostsTab = () => { + const { isEnabled: isTrackRowsEnabled } = useFeatureFlag( + FeatureFlags.TRACK_ROWS + ) const { handle, repost_count = 0 } = useProfileUser({ select: (user) => ({ @@ -45,6 +50,7 @@ export const RepostsTab = () => { return ( { + const { isEnabled: isTrackRowsEnabled } = useFeatureFlag( + FeatureFlags.TRACK_ROWS + ) const { handle, track_count = 0 } = useProfileUser({ select: (user) => ({ @@ -43,6 +48,7 @@ export const TracksTab = () => { return ( { + const { isEnabled: isTrackRowsEnabled } = useFeatureFlag( + FeatureFlags.TRACK_ROWS + ) const [query] = useSearchQuery() const [filters] = useSearchFilters() const { @@ -68,6 +73,7 @@ export const TrackResults = () => { return ( { + const { isEnabled: isTrackRowsEnabled } = useFeatureFlag( + FeatureFlags.TRACK_ROWS + ) const navigation = useNavigation() const dispatch = useDispatch() const genre = useSelector(getTrendingGenre) @@ -75,6 +80,7 @@ export const TrendingLineup = ({ return ( { + const { isEnabled: isTrackRowsEnabled } = useFeatureFlag( + FeatureFlags.TRACK_ROWS + ) const { trackIds, isPending, isFetching, hasNextPage, loadNextPage } = useTrendingUnderground({ pageSize: PAGE_SIZE }) @@ -35,6 +40,7 @@ export const TrendingUndergroundLineup = ({ return (