From 3650747b79c739f8c461c09fcf0ba18fc3e348ec Mon Sep 17 00:00:00 2001 From: Dharit Tantiviramanond Date: Thu, 20 Jun 2024 15:58:03 -0400 Subject: [PATCH 1/4] Split mobile Track vs Collection Details Tiles --- .../components/details-tile/DetailsTile.tsx | 415 ------------------ .../collection-screen/CollectionScreen.tsx | 1 - .../CollectionScreenDetailsTile.tsx | 354 +++++++++++---- .../track-screen/TrackScreenDetailsTile.tsx | 409 +++++++++++++---- 4 files changed, 606 insertions(+), 573 deletions(-) delete mode 100644 packages/mobile/src/components/details-tile/DetailsTile.tsx diff --git a/packages/mobile/src/components/details-tile/DetailsTile.tsx b/packages/mobile/src/components/details-tile/DetailsTile.tsx deleted file mode 100644 index 1ec41a10704..00000000000 --- a/packages/mobile/src/components/details-tile/DetailsTile.tsx +++ /dev/null @@ -1,415 +0,0 @@ -import { useCallback } from 'react' - -import { useGatedContentAccessMap } from '@audius/common/hooks' -import { isContentUSDCPurchaseGated } from '@audius/common/models' -import { - accountSelectors, - playerSelectors, - playbackPositionSelectors, - cacheCollectionsSelectors -} from '@audius/common/store' -import type { CommonState } from '@audius/common/store' -import { getDogEarType, Genre, getLocalTimezone } from '@audius/common/utils' -import moment from 'moment' -import { TouchableOpacity } from 'react-native' -import { useSelector } from 'react-redux' - -import { - Flex, - Text, - IconCalendarMonth, - IconPause, - IconPlay, - IconRepeatOff, - Paper, - spacing, - Button, - Divider, - Box, - MusicBadge, - IconVisibilityHidden -} from '@audius/harmony-native' -import CoSign from 'app/components/co-sign/CoSign' -import { Size } from 'app/components/co-sign/types' -import { UserGeneratedText, DogEar, Tag } from 'app/components/core' -import UserBadges from 'app/components/user-badges' -import { light } from 'app/haptics' -import { useNavigation } from 'app/hooks/useNavigation' -import { makeStyles } from 'app/styles' - -import { OfflineStatusRow } from '../offline-downloads' - -import { CollectionMetadataList } from './CollectionMetadataList' -import { DeletedTile } from './DeletedTile' -import { DetailsProgressInfo } from './DetailsProgressInfo' -import { DetailsTileActionButtons } from './DetailsTileActionButtons' -import { DetailsTileAiAttribution } from './DetailsTileAiAttribution' -import { DetailsTileHasAccess } from './DetailsTileHasAccess' -import { DetailsTileNoAccess } from './DetailsTileNoAccess' -import { DetailsTileStats } from './DetailsTileStats' -import { TrackMetadataList } from './TrackMetadataList' -import type { DetailsTileProps } from './types' - -const { getTrackId } = playerSelectors -const { getTrackPosition } = playbackPositionSelectors -const { getCollectionTracks } = cacheCollectionsSelectors - -const messages = { - play: 'Play', - pause: 'Pause', - resume: 'Resume', - replay: 'Replay', - preview: 'Preview', - hidden: 'Hidden', - releases: (releaseDate: string) => - `Releases ${moment(releaseDate).format( - 'M/D/YY [@] h:mm A' - )} ${getLocalTimezone()}` -} - -const useStyles = makeStyles(({ palette, spacing }) => ({ - coverArt: { - borderWidth: 1, - borderColor: palette.neutralLight8, - borderRadius: spacing(2), - height: 224, - width: 224, - alignSelf: 'center' - } -})) - -/** - * The details shown at the top of the Track Screen and Collection Screen - */ -export const DetailsTile = ({ - contentId, - contentType, - coSign, - description, - descriptionLinkPressSource, - hasReposted, - hasSaved, - hasStreamAccess, - streamConditions, - hideFavorite, - hideFavoriteCount, - hidePlayCount, - hideOverflow, - hideRepost, - hideRepostCount, - hideShare, - isPlaying, - isPreviewing, - isDeleted = false, - isPlayable = true, - isCollection = false, - isPublished = true, - isUnlisted = false, - onPressEdit, - onPressFavorites, - onPressOverflow, - onPressPlay, - onPressPreview, - onPressPublish, - onPressRepost, - onPressReposts, - onPressSave, - onPressShare, - playCount, - renderBottomContent, - renderImage, - repostCount, - saveCount, - headerText, - title, - user, - track, - ddexApp, - releaseDate -}: DetailsTileProps) => { - const styles = useStyles() - const navigation = useNavigation() - - const currentUserId = useSelector(accountSelectors.getUserId) - const isCurrentTrack = useSelector((state: CommonState) => { - return track && track.track_id === getTrackId(state) - }) - - const tracks = useSelector((state: CommonState) => - getCollectionTracks(state, { id: contentId }) - ) - const trackAccessMap = useGatedContentAccessMap(tracks ?? []) - const doesUserHaveAccessToAnyTrack = Object.values(trackAccessMap).some( - ({ hasStreamAccess }) => hasStreamAccess - ) - - const isOwner = user?.user_id === currentUserId - const isLongFormContent = - track?.genre === Genre.PODCASTS || track?.genre === Genre.AUDIOBOOKS - const aiAttributionUserId = track?.ai_attribution_user_id - const isUSDCPurchaseGated = isContentUSDCPurchaseGated(streamConditions) - - const isPlayingPreview = isPreviewing && isPlaying - const isPlayingFullAccess = isPlaying && !isPreviewing - const isUnpublishedScheduledRelease = - track?.is_scheduled_release && track?.is_unlisted && releaseDate - - // Show play if user has access to the collection or any of its contents. - // Show preview only if the user is the owner on a track screen. - const shouldShowPlay = - (isPlayable && hasStreamAccess) || doesUserHaveAccessToAnyTrack - const shouldShowPreview = - isUSDCPurchaseGated && - ((!isCollection && isOwner) || // own track - (isCollection && !hasStreamAccess && !shouldShowPlay) || // premium collection - (!isCollection && !hasStreamAccess)) && // premium track - !!onPressPreview - - const handlePressArtistName = useCallback(() => { - if (!user) { - return - } - navigation.push('Profile', { handle: user.handle }) - }, [navigation, user]) - - const handlePressPlay = useCallback(() => { - light() - onPressPlay() - }, [onPressPlay]) - - const handlePressPreview = useCallback(() => { - light() - onPressPreview?.() - }, [onPressPreview]) - - const renderDogEar = () => { - const dogEarType = getDogEarType({ - isOwner, - streamConditions - }) - return dogEarType ? : null - } - - const innerImageElement = renderImage({ - style: styles.coverArt - }) - - const imageElement = coSign ? ( - {innerImageElement} - ) : ( - innerImageElement - ) - - const playbackPositionInfo = useSelector((state) => - getTrackPosition(state, { trackId: contentId, userId: currentUserId }) - ) - - const playText = playbackPositionInfo?.status - ? playbackPositionInfo?.status === 'IN_PROGRESS' || isCurrentTrack - ? messages.resume - : messages.replay - : messages.play - - const PlayIcon = - playbackPositionInfo?.status === 'COMPLETED' && !isCurrentTrack - ? IconRepeatOff - : IconPlay - - const PreviewButton = () => ( - - ) - - const badges = [ - aiAttributionUserId ? ( - - ) : null, - isUnpublishedScheduledRelease ? ( - - {messages.releases(releaseDate)} - - ) : isUnlisted ? ( - {messages.hidden} - ) : null - ].filter((badge) => badge !== null) - - const handlePressTag = useCallback( - (tag: string) => { - navigation.push('TagSearch', { query: tag }) - }, - [navigation] - ) - - const renderTags = () => { - if (!track || (isUnlisted && !track.field_visibility?.tags)) { - return null - } - - const filteredTags = (track.tags || '').split(',').filter(Boolean) - return filteredTags.length > 0 ? ( - - {filteredTags.map((tag) => ( - handlePressTag(tag)}> - {tag} - - ))} - - ) : null - } - - if (isDeleted) { - return ( - - ) - } - - return ( - - {renderDogEar()} - - - {headerText} - - - {badges.length > 0 ? ( - - {badges.map((badge) => badge)} - - ) : null} - {imageElement} - - - {title} - - {user ? ( - - - - {user.name} - - - - - ) : null} - - {isLongFormContent && track ? ( - - ) : null} - {shouldShowPlay ? ( - - ) : null} - {shouldShowPreview ? : null} - - - - {!hasStreamAccess && !isOwner && streamConditions && contentId ? ( - - ) : null} - {(hasStreamAccess || isOwner) && streamConditions ? ( - - ) : null} - {!isPublished ? null : ( - - )} - {description ? ( - - - {description} - - - ) : null} - {isCollection ? ( - contentId ? ( - - ) : null - ) : contentId ? ( - - ) : null} - {renderTags()} - - - - {renderBottomContent?.()} - - ) -} diff --git a/packages/mobile/src/screens/collection-screen/CollectionScreen.tsx b/packages/mobile/src/screens/collection-screen/CollectionScreen.tsx index 6249e251820..b0ce57137f0 100644 --- a/packages/mobile/src/screens/collection-screen/CollectionScreen.tsx +++ b/packages/mobile/src/screens/collection-screen/CollectionScreen.tsx @@ -291,7 +291,6 @@ const CollectionScreenComponent = (props: CollectionScreenComponentProps) => { hasSaved={has_current_user_saved} isAlbum={is_album} collectionId={playlist_id} - isPrivate={is_private} isPublishing={_is_publishing ?? false} isDeleted={is_delete} onPressEdit={handlePressEdit} diff --git a/packages/mobile/src/screens/collection-screen/CollectionScreenDetailsTile.tsx b/packages/mobile/src/screens/collection-screen/CollectionScreenDetailsTile.tsx index ac48924b1a2..aa0a9133518 100644 --- a/packages/mobile/src/screens/collection-screen/CollectionScreenDetailsTile.tsx +++ b/packages/mobile/src/screens/collection-screen/CollectionScreenDetailsTile.tsx @@ -1,13 +1,19 @@ import { useCallback, useEffect } from 'react' import { useGetCurrentUserId, useGetPlaylistById } from '@audius/common/api' -import { Name, PlaybackSource, Status } from '@audius/common/models' +import { + Name, + PlaybackSource, + Status, + isContentUSDCPurchaseGated +} from '@audius/common/models' import type { SmartCollectionVariant, ID, UID, AccessConditions } from '@audius/common/models' +import type { CommonState } from '@audius/common/store' import { cacheCollectionsSelectors, collectionPageLineupActions as tracksActions, @@ -18,20 +24,40 @@ import { PurchaseableContentType, queueActions } from '@audius/common/store' -import { removeNullable } from '@audius/common/utils' +import { getDogEarType, getLocalTimezone } from '@audius/common/utils' import type { Maybe, Nullable } from '@audius/common/utils' +import dayjs from 'dayjs' +import { TouchableOpacity } from 'react-native' import { useDispatch, useSelector } from 'react-redux' import { usePrevious } from 'react-use' import { createSelector } from 'reselect' +import { useGatedContentAccessMap } from '~/hooks' -import { Box } from '@audius/harmony-native' -import { Text } from 'app/components/core' -import { DetailsTile } from 'app/components/details-tile' -import type { - DetailsTileDetail, - DetailsTileProps -} from 'app/components/details-tile/types' +import { + Box, + Button, + Divider, + Flex, + IconCalendarMonth, + IconPause, + IconPlay, + IconVisibilityHidden, + MusicBadge, + Paper, + Text, + spacing +} from '@audius/harmony-native' +import { DogEar, UserGeneratedText } from 'app/components/core' +import { CollectionMetadataList } from 'app/components/details-tile/CollectionMetadataList' +import { DetailsTileActionButtons } from 'app/components/details-tile/DetailsTileActionButtons' +import { DetailsTileHasAccess } from 'app/components/details-tile/DetailsTileHasAccess' +import { DetailsTileNoAccess } from 'app/components/details-tile/DetailsTileNoAccess' +import { DetailsTileStats } from 'app/components/details-tile/DetailsTileStats' +import type { DetailsTileProps } from 'app/components/details-tile/types' +import { OfflineStatusRow } from 'app/components/offline-downloads' import { TrackList } from 'app/components/track-list' +import UserBadges from 'app/components/user-badges' +import { useNavigation } from 'app/hooks/useNavigation' import { make, track } from 'app/services/analytics' import type { AppState } from 'app/store' import { makeStyles } from 'app/styles' @@ -39,7 +65,7 @@ import { makeStyles } from 'app/styles' const { getPlaying, getPreviewing, getUid, getCurrentTrack } = playerSelectors const { getIsReachable } = reachabilitySelectors const { getCollectionTracksLineup } = collectionPageSelectors -const { getCollection } = cacheCollectionsSelectors +const { getCollection, getCollectionTracks } = cacheCollectionsSelectors const { getTracks } = cacheTracksSelectors const selectTrackUids = createSelector( @@ -58,19 +84,6 @@ const selectIsLineupLoading = (state: AppState) => { return getCollectionTracksLineup(state).status !== Status.SUCCESS } -const selectCollectionDuration = createSelector( - (state: AppState) => getCollectionTracksLineup(state).entries, - (state: AppState) => state.tracks.entries, - (entries, tracks) => { - return entries - .map((entry) => tracks[entry.id]?.metadata.duration) - .filter(removeNullable) - .reduce((totalDuration, trackDuration) => { - return totalDuration + trackDuration - }, 0) - } -) - const selectIsQueued = createSelector( selectTrackUids, getUid, @@ -107,7 +120,17 @@ const getMessages = ( emptyPublic: `This ${collectionType} is empty`, detailsPlaceholder: '---', collectionType: `${isPremium ? 'premium ' : ''}${collectionType}`, - hiddenType: `Hidden ${collectionType}` + hiddenType: `Hidden ${collectionType}`, + play: 'Play', + pause: 'Pause', + resume: 'Resume', + replay: 'Replay', + preview: 'Preview', + hidden: 'Hidden', + releases: (releaseDate: string) => + `Releases ${dayjs(releaseDate).format( + 'M/D/YY [@] h:mm A' + )} ${getLocalTimezone()}` }) const useStyles = makeStyles(({ palette, spacing }) => ({ @@ -117,16 +140,22 @@ const useStyles = makeStyles(({ palette, spacing }) => ({ marginBottom: spacing(8), textAlign: 'center', lineHeight: 20 + }, + coverArt: { + borderWidth: 1, + borderColor: palette.neutralLight8, + borderRadius: spacing(2), + height: 224, + width: 224, + alignSelf: 'center' } })) type CollectionScreenDetailsTileProps = { isAlbum?: boolean - isPrivate?: boolean isOwner?: boolean isPublishing?: boolean isDeleted?: boolean - extraDetails?: DetailsTileDetail[] collectionId: number | SmartCollectionVariant hasStreamAccess?: boolean streamConditions?: Nullable @@ -153,25 +182,42 @@ const recordPlay = (id: Maybe, play = true) => { export const CollectionScreenDetailsTile = ({ description, - extraDetails = [], collectionId, isAlbum, - isPrivate, isPublishing, renderImage, trackCount: trackCountProp, - isOwner, + isOwner = false, hideOverflow, hideRepost, hideFavorite, hasStreamAccess, streamConditions, ddexApp, - isDeleted, - ...detailsTileProps + hideShare, + playCount, + hidePlayCount, + hideFavoriteCount, + hideRepostCount, + repostCount, + saveCount, + hasSaved, + title, + hasReposted, + onPressEdit, + onPressFavorites, + onPressOverflow, + onPressPublish, + onPressRepost, + onPressReposts, + onPressSave, + onPressShare, + user, + releaseDate }: CollectionScreenDetailsTileProps) => { const styles = useStyles() const dispatch = useDispatch() + const navigation = useNavigation() const isReachable = useSelector(getIsReachable) @@ -186,23 +232,73 @@ export const CollectionScreenDetailsTile = ({ }, { disabled: typeof collectionId !== 'number' } ) - const isStreamGated = collection?.is_stream_gated + const { + is_stream_gated: isStreamGated, + is_scheduled_release: isScheduledRelease, + is_private: isPrivate + } = collection ?? {} + const numericCollectionId = + typeof collectionId === 'number' ? collectionId : undefined + const collectionTracks = useSelector((state: CommonState) => + getCollectionTracks(state, { id: numericCollectionId }) + ) + const trackAccessMap = useGatedContentAccessMap(collectionTracks ?? []) + const doesUserHaveAccessToAnyTrack = Object.values(trackAccessMap).some( + ({ hasStreamAccess }) => hasStreamAccess + ) const trackUids = useSelector(selectTrackUids) const collectionTrackCount = useSelector(selectTrackCount) const trackCount = trackCountProp ?? collectionTrackCount const isLineupLoading = useSelector(selectIsLineupLoading) - const collectionDuration = useSelector(selectCollectionDuration) const playingUid = useSelector(getUid) const isQueued = useSelector(selectIsQueued) const isPlaying = useSelector(getPlaying) const isPreviewing = useSelector(getPreviewing) + const isPlayingPreview = isPreviewing && isPlaying const playingTrack = useSelector(getCurrentTrack) const playingTrackId = playingTrack?.track_id const firstTrack = useSelector(selectFirstTrack) const messages = getMessages(isAlbum ? 'album' : 'playlist', isStreamGated) + const headerText = isPrivate ? messages.hiddenType : messages.collectionType + const isPublished = !isPrivate || isPublishing + const isUnpublishedScheduledRelease = + isScheduledRelease && isPrivate && releaseDate + const shouldHideOverflow = + hideOverflow || !isReachable || (isPrivate && !isOwner) + const isUSDCPurchaseGated = isContentUSDCPurchaseGated(streamConditions) + + const uids = isLineupLoading ? Array(Math.min(5, trackCount ?? 0)) : trackUids + const tracks = useSelector((state) => getTracks(state, { uids })) + const areAllTracksDeleted = Object.values(tracks).every( + (track) => track.is_delete + ) + const isPlayable = + Object.values(tracks).length === 0 + ? true + : !areAllTracksDeleted && (isQueued || (trackCount > 0 && !!firstTrack)) + + const shouldShowPlay = + (isPlayable && hasStreamAccess) || doesUserHaveAccessToAnyTrack + const shouldShowPreview = + isUSDCPurchaseGated && !hasStreamAccess && !shouldShowPlay + useRefetchLineupOnTrackAdd(collectionId) + const badges = [ + isUnpublishedScheduledRelease ? ( + + {messages.releases(releaseDate)} + + ) : isPrivate ? ( + {messages.hidden} + ) : null + ].filter((badge) => badge !== null) + + const imageElement = renderImage({ + style: styles.coverArt + }) + const play = useCallback( ({ isPreview = false }: { isPreview?: boolean } = {}) => { if (isPlaying && isQueued && isPreviewing === isPreview) { @@ -250,19 +346,6 @@ export const CollectionScreenDetailsTile = ({ [dispatch, isPlaying, playingUid] ) - const numericCollectionId = - typeof collectionId === 'number' ? collectionId : undefined - - const uids = isLineupLoading ? Array(Math.min(5, trackCount ?? 0)) : trackUids - const tracks = useSelector((state) => getTracks(state, { uids })) - const areAllTracksDeleted = Object.values(tracks).every( - (track) => track.is_delete - ) - const isPlayable = - Object.values(tracks).length === 0 - ? true - : !areAllTracksDeleted && (isQueued || (trackCount > 0 && !!firstTrack)) - const renderTrackList = useCallback(() => { return ( - + {isOwner ? messages.empty : messages.emptyPublic} @@ -294,33 +377,156 @@ export const CollectionScreenDetailsTile = ({ messages ]) + const renderDogEar = () => { + const dogEarType = getDogEarType({ + isOwner, + streamConditions + }) + return dogEarType ? : null + } + + const handlePressArtistName = useCallback(() => { + if (!user) { + return + } + navigation.push('Profile', { handle: user.handle }) + }, [navigation, user]) + + const PreviewButton = () => ( + + ) + return ( - + + {renderDogEar()} + + + {headerText} + + + {badges.length > 0 ? ( + + {badges.map((badge) => badge)} + + ) : null} + {imageElement} + + + {title} + + {user ? ( + + + + {user.name} + + + + + ) : null} + + {shouldShowPlay ? ( + + ) : null} + {shouldShowPreview ? : null} + + + + {!hasStreamAccess && + !isOwner && + streamConditions && + numericCollectionId ? ( + + ) : null} + {(hasStreamAccess || isOwner) && streamConditions ? ( + + ) : null} + {!isPublished ? null : ( + + )} + {description ? ( + + + {description} + + + ) : null} + {numericCollectionId ? ( + + ) : null} + + + + {renderTrackList()} + ) } diff --git a/packages/mobile/src/screens/track-screen/TrackScreenDetailsTile.tsx b/packages/mobile/src/screens/track-screen/TrackScreenDetailsTile.tsx index 3fdf5a6f474..0164ca98875 100644 --- a/packages/mobile/src/screens/track-screen/TrackScreenDetailsTile.tsx +++ b/packages/mobile/src/screens/track-screen/TrackScreenDetailsTile.tsx @@ -1,4 +1,4 @@ -import { useCallback } from 'react' +import React, { useCallback } from 'react' import { useGatedContentAccess, @@ -11,7 +11,8 @@ import { FavoriteSource, PlaybackSource, FavoriteType, - SquareSizes + SquareSizes, + isContentUSDCPurchaseGated } from '@audius/common/models' import type { UID, @@ -20,7 +21,7 @@ import type { Track, User } from '@audius/common/models' -import { FeatureFlags } from '@audius/common/services' +import type { CommonState } from '@audius/common/store' import { accountSelectors, trackPageLineupActions, @@ -38,16 +39,49 @@ import { playbackPositionSelectors, PurchaseableContentType } from '@audius/common/store' -import { Genre, removeNullable } from '@audius/common/utils' +import { + Genre, + getDogEarType, + getLocalTimezone, + removeNullable +} from '@audius/common/utils' +import dayjs from 'dayjs' +import { TouchableOpacity } from 'react-native' import { useDispatch, useSelector } from 'react-redux' import { trpc } from 'utils/trpcClientWeb' -import type { ImageProps } from '@audius/harmony-native' -import { DetailsTile } from 'app/components/details-tile' +import { + Box, + Button, + Divider, + Flex, + IconCalendarMonth, + IconPause, + IconPlay, + IconRepeatOff, + IconVisibilityHidden, + MusicBadge, + Paper, + Text, + spacing, + type ImageProps +} from '@audius/harmony-native' +import CoSign, { Size } from 'app/components/co-sign' +import { DogEar, Tag, UserGeneratedText } from 'app/components/core' +import { DeletedTile } from 'app/components/details-tile/DeletedTile' +import { DetailsProgressInfo } from 'app/components/details-tile/DetailsProgressInfo' +import { DetailsTileActionButtons } from 'app/components/details-tile/DetailsTileActionButtons' +import { DetailsTileAiAttribution } from 'app/components/details-tile/DetailsTileAiAttribution' +import { DetailsTileHasAccess } from 'app/components/details-tile/DetailsTileHasAccess' +import { DetailsTileNoAccess } from 'app/components/details-tile/DetailsTileNoAccess' +import { DetailsTileStats } from 'app/components/details-tile/DetailsTileStats' +import { TrackMetadataList } from 'app/components/details-tile/TrackMetadataList' import { TrackImage } from 'app/components/image/TrackImage' +import { OfflineStatusRow } from 'app/components/offline-downloads' +import UserBadges from 'app/components/user-badges' import { useNavigation } from 'app/hooks/useNavigation' -import { useFeatureFlag } from 'app/hooks/useRemoteConfig' import { make, track as record } from 'app/services/analytics' +import { makeStyles } from 'app/styles' import { DownloadSection } from './DownloadSection' const { getPlaying, getTrackId, getPreviewing } = playerSelectors @@ -73,9 +107,30 @@ const messages = { specialAccess: 'special access', premiumTrack: 'premium track', generatedWithAi: 'generated with ai', - trackDeleted: 'track [deleted by artist]' + trackDeleted: 'track [deleted by artist]', + play: 'Play', + pause: 'Pause', + resume: 'Resume', + replay: 'Replay', + preview: 'Preview', + hidden: 'Hidden', + releases: (releaseDate: string) => + `Releases ${dayjs(releaseDate).format( + 'M/D/YY [@] h:mm A' + )} ${getLocalTimezone()}` } +const useStyles = makeStyles(({ palette, spacing }) => ({ + coverArt: { + borderWidth: 1, + borderColor: palette.neutralLight8, + borderRadius: spacing(2), + height: 224, + width: 224, + alignSelf: 'center' + } +})) + type TrackScreenDetailsTileProps = { track: Track | SearchTrack user: User | SearchUser @@ -100,11 +155,8 @@ export const TrackScreenDetailsTile = ({ uid, isLineupLoading }: TrackScreenDetailsTileProps) => { + const styles = useStyles() const { hasStreamAccess } = useGatedContentAccess(track as Track) // track is of type Track | SearchTrack but we only care about some of their common fields, maybe worth refactoring later - const { isEnabled: isNewPodcastControlsEnabled } = useFeatureFlag( - FeatureFlags.PODCAST_CONTROL_UPDATES_ENABLED, - FeatureFlags.PODCAST_CONTROL_UPDATES_ENABLED_FALLBACK - ) const navigation = useNavigation() const isReachable = useSelector(getIsReachable) @@ -114,35 +166,42 @@ export const TrackScreenDetailsTile = ({ const isPlaying = useSelector(getPlaying) const isPreviewing = useSelector(getPreviewing) const isPlayingId = playingId === track.track_id + const playbackPositionInfo = useSelector((state) => + getTrackPosition(state, { trackId, userId: currentUserId }) + ) + const isCurrentTrack = useSelector((state: CommonState) => { + return track && track.track_id === getTrackId(state) + }) const { - _co_sign, + _co_sign: coSign, description, genre, - has_current_user_reposted, - has_current_user_saved, + has_current_user_reposted: hasReposted, + has_current_user_saved: hasSaved, is_unlisted: isUnlisted, is_stream_gated: isStreamGated, - owner_id, - play_count, - remix_of, - repost_count, - save_count, + owner_id: ownerId, + play_count: playCount, + remix_of: remixOf, + repost_count: repostCount, + save_count: saveCount, title, track_id: trackId, stream_conditions: streamConditions, ddex_app: ddexApp, - is_delete, - duration, + is_delete: isDeleted, release_date: releaseDate, is_scheduled_release: isScheduledRelease } = track - const isOwner = owner_id === currentUserId + const isOwner = ownerId === currentUserId const hideFavorite = isUnlisted || !hasStreamAccess const hideRepost = isUnlisted || !isReachable || !hasStreamAccess + const hideOverflow = !isReachable || (isUnlisted && !isOwner) + const hideShare = isUnlisted && !isOwner - const remixParentTrackId = remix_of?.tracks?.[0]?.parent_track_id + const remixParentTrackId = remixOf?.tracks?.[0]?.parent_track_id const isRemix = !!remixParentTrackId const hasDownloadableAssets = (track as Track)?.is_downloadable || @@ -154,12 +213,70 @@ export const TrackScreenDetailsTile = ({ { enabled: !!trackId } ) + const isLongFormContent = + track?.genre === Genre.PODCASTS || track?.genre === Genre.AUDIOBOOKS + const aiAttributionUserId = track?.ai_attribution_user_id + const isUSDCPurchaseGated = isContentUSDCPurchaseGated(streamConditions) + + const isPlayingPreview = isPreviewing && isPlaying + const isPlayingFullAccess = isPlaying && !isPreviewing + const isUnpublishedScheduledRelease = + track?.is_scheduled_release && track?.is_unlisted && releaseDate + const shouldShowPreview = isUSDCPurchaseGated && (isOwner || !hasStreamAccess) + const shouldHideFavoriteCount = + isUnlisted || (!isOwner && (saveCount ?? 0) <= 0) + const shouldHideRepostCount = + isUnlisted || (!isOwner && (repostCount ?? 0) <= 0) + const shouldHidePlayCount = + (!isOwner && isUnlisted) || + isStreamGated || + (!isOwner && (playCount ?? 0) <= 0) + + const headerText = isRemix + ? messages.remix + : isStreamGated + ? messages.premiumTrack + : messages.track + + const PlayIcon = + playbackPositionInfo?.status === 'COMPLETED' && !isCurrentTrack + ? IconRepeatOff + : IconPlay + + const badges = [ + aiAttributionUserId ? ( + + ) : null, + isUnpublishedScheduledRelease ? ( + + {messages.releases(releaseDate)} + + ) : isUnlisted ? ( + {messages.hidden} + ) : null + ].filter((badge) => badge !== null) + + const playText = playbackPositionInfo?.status + ? playbackPositionInfo?.status === 'IN_PROGRESS' || isCurrentTrack + ? messages.resume + : messages.replay + : messages.play + const renderImage = useCallback( (props: ImageProps) => ( ), [track] ) + const innerImageElement = renderImage({ + style: styles.coverArt + }) + + const imageElement = coSign ? ( + {innerImageElement} + ) : ( + innerImageElement + ) const currentQueueItem = useSelector(getCurrentQueueItem) const play = useCallback( @@ -214,7 +331,7 @@ export const TrackScreenDetailsTile = ({ const handlePressSave = () => { if (!isOwner) { - if (has_current_user_saved) { + if (hasSaved) { dispatch(unsaveTrack(trackId, FavoriteSource.TRACK_PAGE)) } else { dispatch(saveTrack(trackId, FavoriteSource.TRACK_PAGE)) @@ -224,7 +341,7 @@ export const TrackScreenDetailsTile = ({ const handlePressRepost = () => { if (!isOwner) { - if (has_current_user_reposted) { + if (hasReposted) { dispatch(undoRepostTrack(trackId, RepostSource.TRACK_PAGE)) } else { dispatch(repostTrack(trackId, RepostSource.TRACK_PAGE)) @@ -242,13 +359,24 @@ export const TrackScreenDetailsTile = ({ ) } + const handlePressArtistName = useCallback(() => { + if (!user) { + return + } + navigation.push('Profile', { handle: user.handle }) + }, [navigation, user]) + + const handlePressTag = useCallback( + (tag: string) => { + navigation.push('TagSearch', { query: tag }) + }, + [navigation] + ) + const handlePressEdit = useCallback(() => { navigation?.push('EditTrack', { id: trackId }) }, [navigation, trackId]) - const playbackPositionInfo = useSelector((state) => - getTrackPosition(state, { trackId, userId: currentUserId }) - ) const handlePressOverflow = () => { const isLongFormContent = genre === Genre.PODCASTS || genre === Genre.AUDIOBOOKS @@ -265,7 +393,7 @@ export const TrackScreenDetailsTile = ({ : user.does_current_user_follow ? OverflowAction.UNFOLLOW_ARTIST : OverflowAction.FOLLOW_ARTIST, - isNewPodcastControlsEnabled && isLongFormContent + isLongFormContent ? playbackPositionInfo?.status === 'COMPLETED' ? OverflowAction.MARK_AS_UNPLAYED : OverflowAction.MARK_AS_PLAYED @@ -292,59 +420,174 @@ export const TrackScreenDetailsTile = ({ return hasDownloadableAssets ? : null } + const renderDogEar = () => { + const dogEarType = getDogEarType({ + isOwner, + streamConditions + }) + return dogEarType ? : null + } + + const PreviewButton = () => ( + + ) + + const renderTags = () => { + if (!track || (isUnlisted && !track.field_visibility?.tags)) { + return null + } + + const filteredTags = (track.tags || '').split(',').filter(Boolean) + return filteredTags.length > 0 ? ( + + {filteredTags.map((tag) => ( + handlePressTag(tag)}> + {tag} + + ))} + + ) : null + } + + if (!trackId) return null + + if (isDeleted) { + return ( + + ) + } + return ( - + + {renderDogEar()} + + + {headerText} + + + {badges.length > 0 ? ( + + {badges.map((badge) => badge)} + + ) : null} + {imageElement} + + + {title} + + {user ? ( + + + + {user.name} + + + + + ) : null} + + {isLongFormContent && track ? ( + + ) : null} + {hasStreamAccess ? ( + + ) : null} + {shouldShowPreview ? : null} + + + + {!hasStreamAccess && !isOwner && streamConditions && trackId ? ( + + ) : null} + {(hasStreamAccess || isOwner) && streamConditions ? ( + + ) : null} + + {description ? ( + + + {description} + + + ) : null} + + {renderTags()} + + + + {renderBottomContent?.()} + ) } From 04051f59908a9bf786bd092239ad134e3ad0d5d4 Mon Sep 17 00:00:00 2001 From: Dharit Tantiviramanond Date: Thu, 20 Jun 2024 16:11:09 -0400 Subject: [PATCH 2/4] delete details-tile/index.ts --- packages/mobile/src/components/details-tile/index.ts | 1 - 1 file changed, 1 deletion(-) delete mode 100644 packages/mobile/src/components/details-tile/index.ts diff --git a/packages/mobile/src/components/details-tile/index.ts b/packages/mobile/src/components/details-tile/index.ts deleted file mode 100644 index 9762470b4a2..00000000000 --- a/packages/mobile/src/components/details-tile/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { DetailsTile } from './DetailsTile' From 1aac2e386ecd709e18bf9f1e522f36eab8bbf739 Mon Sep 17 00:00:00 2001 From: Dharit Tantiviramanond Date: Thu, 20 Jun 2024 16:22:03 -0400 Subject: [PATCH 3/4] remove extraDetails --- .../src/screens/collection-screen/CollectionScreen.tsx | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/packages/mobile/src/screens/collection-screen/CollectionScreen.tsx b/packages/mobile/src/screens/collection-screen/CollectionScreen.tsx index b0ce57137f0..c9865678800 100644 --- a/packages/mobile/src/screens/collection-screen/CollectionScreen.tsx +++ b/packages/mobile/src/screens/collection-screen/CollectionScreen.tsx @@ -173,15 +173,6 @@ const CollectionScreenComponent = (props: CollectionScreenComponentProps) => { const currentUserId = useSelector(getUserId) const isOwner = currentUserId === playlist_owner_id - const extraDetails = useMemo( - () => [ - { - label: 'Modified', - value: formatDate(updated_at || Date.now().toString()) - } - ], - [updated_at] - ) const isCollectionMarkedForDownload = useSelector( getIsCollectionMarkedForDownload(playlist_id.toString()) @@ -286,7 +277,6 @@ const CollectionScreenComponent = (props: CollectionScreenComponentProps) => { <> Date: Thu, 20 Jun 2024 16:36:58 -0400 Subject: [PATCH 4/4] lint --- .../mobile/src/screens/collection-screen/CollectionScreen.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mobile/src/screens/collection-screen/CollectionScreen.tsx b/packages/mobile/src/screens/collection-screen/CollectionScreen.tsx index c9865678800..3ff24677a8e 100644 --- a/packages/mobile/src/screens/collection-screen/CollectionScreen.tsx +++ b/packages/mobile/src/screens/collection-screen/CollectionScreen.tsx @@ -28,7 +28,7 @@ import { favoritesUserListActions, RepostType } from '@audius/common/store' -import { encodeUrlName, formatDate, removeNullable } from '@audius/common/utils' +import { encodeUrlName, removeNullable } from '@audius/common/utils' import type { Nullable } from '@audius/common/utils' import { useDispatch, useSelector } from 'react-redux'