Skip to content
This repository has been archived by the owner on Oct 4, 2023. It is now read-only.

[PAY-1591][PAY-1775] Implements content previews on native mobile #3986

Merged
merged 6 commits into from
Aug 31, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion packages/common/src/store/player/slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ type SetBufferingPayload = {
type SetPayload = {
uid: UID
trackId: ID
previewing?: boolean
}

type SeekPayload = {
Expand Down Expand Up @@ -139,6 +140,7 @@ const slice = createSlice({
action: PayloadAction<PlayCollectibleSucceededPayload>
) => {
const { collectible } = action.payload
state.previewing = false
state.playing = true
state.uid = null
state.trackId = null
Expand All @@ -158,15 +160,17 @@ const slice = createSlice({
state.counter = state.counter + 1
},
set: (state, action: PayloadAction<SetPayload>) => {
const { uid, trackId } = action.payload
const { previewing, uid, trackId } = action.payload
state.uid = uid
state.trackId = trackId
state.previewing = !!previewing
},
reset: (_state, _action: PayloadAction<ResetPayload>) => {},
resetSucceeded: (state, action: PayloadAction<ResetSucceededPayload>) => {
const { shouldAutoplay } = action.payload
state.playing = shouldAutoplay
state.counter = state.counter + 1
state.previewing = false
},
seek: (state, action: PayloadAction<SeekPayload>) => {
const { seconds } = action.payload
Expand Down
1 change: 1 addition & 0 deletions packages/common/src/store/queue/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export type Queueable = {
uid: UID
artistId?: ID
collectible?: Collectible
isPreview?: boolean
source: QueueSource
}

Expand Down
7 changes: 7 additions & 0 deletions packages/common/src/utils/streaming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { Nullable } from './typeUtils'

const { getPremiumTrackSignatureMap } = premiumContentSelectors

const PREVIEW_LENGTH_SECONDS = 30

export async function generateUserSignature(
audiusBackendInstance: AudiusBackend
) {
Expand Down Expand Up @@ -50,3 +52,8 @@ export function* doesUserHaveTrackAccess(track: Nullable<Track>) {

return !isPremium || hasPremiumContentSignature
}

export function getTrackPreviewDuration(track: Track) {
const previewStartSeconds = track.preview_start_seconds || 0
return Math.min(track.duration - previewStartSeconds, PREVIEW_LENGTH_SECONDS)
}
63 changes: 38 additions & 25 deletions packages/mobile/src/components/audio/Audio.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
ID,
Nullable,
QueryParams,
Queueable,
Track
} from '@audius/common'
import {
Expand All @@ -30,7 +31,8 @@ import {
SquareSizes,
shallowCompare,
savedPageTracksLineupActions,
useAppContext
useAppContext,
getTrackPreviewDuration
} from '@audius/common'
import { isEqual } from 'lodash'
import TrackPlayer, {
Expand Down Expand Up @@ -149,6 +151,10 @@ const unlistedTrackFallbackTrackData = {
duration: 0
}

type QueueableTrack = {
track: Nullable<Track>
} & Pick<Queueable, 'isPreview'>

export const Audio = () => {
const { isEnabled: isNewPodcastControlsEnabled } = useFeatureFlag(
FeatureFlags.PODCAST_CONTROL_UPDATES_ENABLED,
Expand Down Expand Up @@ -184,11 +190,12 @@ export const Audio = () => {
(state) => getTracks(state, { uids: queueTrackUids }),
shallowCompare
)
const queueTracks = queueOrder.map(
(trackData) => queueTrackMap[trackData.id] as Nullable<Track>
)
const queueTracks: QueueableTrack[] = queueOrder.map(({ id, isPreview }) => ({
track: queueTrackMap[id] as Nullable<Track>,
isPreview
}))
const queueTrackOwnerIds = queueTracks
.map((track) => track?.owner_id)
.map(({ track }) => track?.owner_id)
.filter(removeNullable)

const queueTrackOwnersMap = useSelector(
Expand Down Expand Up @@ -244,8 +251,16 @@ export const Audio = () => {
[dispatch]
)
const updatePlayerInfo = useCallback(
({ trackId, uid }: { trackId: number; uid: string }) => {
dispatch(playerActions.set({ trackId, uid }))
({
previewing,
trackId,
uid
}: {
previewing: boolean
trackId: number
uid: string
}) => {
dispatch(playerActions.set({ previewing, trackId, uid }))
},
[dispatch]
)
Expand Down Expand Up @@ -278,10 +293,10 @@ export const Audio = () => {
}>({})

const handleGatedQueryParams = useCallback(
async (tracks: Nullable<Track>[]) => {
async (tracks: QueueableTrack[]) => {
const queryParamsMap: { [trackId: ID]: QueryParams } = {}

for (const track of tracks) {
for (const { track } of tracks) {
if (!track) {
continue
}
Expand Down Expand Up @@ -355,7 +370,7 @@ export const Audio = () => {
// Figure out how to call next earlier
next()
} else {
const track = queueTracks[playerIndex]
const { track, isPreview } = queueTracks[playerIndex]

// Skip track if user does not have access i.e. for an unlocked premium track
const doesUserHaveAccess = (() => {
Expand All @@ -381,6 +396,7 @@ export const Audio = () => {
// Update queue info and handle playback position updates
updateQueueIndex(playerIndex)
updatePlayerInfo({
previewing: !!isPreview,
trackId: track.track_id,
uid: queueTrackUids[playerIndex]
})
Expand Down Expand Up @@ -410,8 +426,8 @@ export const Audio = () => {
}

const isLongFormContent =
queueTracks[playerIndex]?.genre === Genre.PODCASTS ||
queueTracks[playerIndex]?.genre === Genre.AUDIOBOOKS
queueTracks[playerIndex].track?.genre === Genre.PODCASTS ||
queueTracks[playerIndex].track?.genre === Genre.AUDIOBOOKS
if (isLongFormContent !== isLongFormContentRef.current) {
isLongFormContentRef.current = isLongFormContent
// Update playback rate based on if the track is a podcast or not
Expand All @@ -429,7 +445,7 @@ export const Audio = () => {
event?.position !== null &&
event?.track !== null
) {
const track = queueTracks[event.track]
const { track } = queueTracks[event.track]
const isLongFormContent =
track?.genre === Genre.PODCASTS || track?.genre === Genre.AUDIOBOOKS
const isAtEndOfTrack =
Expand Down Expand Up @@ -574,7 +590,7 @@ export const Audio = () => {
? await handleGatedQueryParams(newQueueTracks)
: null

const newTrackData = newQueueTracks.map((track) => {
const newTrackData = newQueueTracks.map(({ track, isPreview }) => {
if (!track) {
return unlistedTrackFallbackTrackData
}
Expand All @@ -589,17 +605,14 @@ export const Audio = () => {
const audioFilePath = getLocalAudioPath(trackId)
url = `file://${audioFilePath}`
} else {
const queryParams = queryParamsMap?.[track.track_id]
if (queryParams) {
url = apiClient.makeUrl(
`/tracks/${encodeHashId(track.track_id)}/stream`,
queryParams
)
} else {
url = apiClient.makeUrl(
`/tracks/${encodeHashId(track.track_id)}/stream`
)
const queryParams = {
...queryParamsMap?.[track.track_id],
preview: isPreview
}
url = apiClient.makeUrl(
`/tracks/${encodeHashId(track.track_id)}/stream`,
queryParams
)
}

const localTrackImageSource =
Expand Down Expand Up @@ -627,7 +640,7 @@ export const Audio = () => {
genre: track.genre,
date: track.created_at,
artwork: imageUrl,
duration: track?.duration
duration: isPreview ? getTrackPreviewDuration(track) : track.duration
}
})

Expand Down
29 changes: 19 additions & 10 deletions packages/mobile/src/components/details-tile/DetailsTile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ export const DetailsTile = ({
hideRepostCount,
hideShare,
isPlaying,
isPreviewing,
isPlayable = true,
isPlaylist = false,
isPublished = true,
Expand All @@ -169,6 +170,7 @@ export const DetailsTile = ({
onPressFavorites,
onPressOverflow,
onPressPlay,
onPressPreview,
onPressPublish,
onPressRepost,
onPressReposts,
Expand Down Expand Up @@ -213,6 +215,12 @@ export const DetailsTile = ({
const isUSDCPurchaseGated =
isPremiumContentUSDCPurchaseGated(premiumConditions)

const isPlayingPreview = isPreviewing && isPlaying
const isPlayingFullAccess = isPlaying && !isPreviewing

const showPreviewButton =
isUSDCPurchaseGated && (isOwner || !doesUserHaveAccess) && onPressPreview

const handlePressArtistName = useCallback(() => {
if (!user) {
return
Expand All @@ -229,6 +237,11 @@ export const DetailsTile = ({
onPressPlay()
}, [onPressPlay])

const handlePressPreview = useCallback(() => {
light()
onPressPreview?.()
}, [onPressPreview])

const renderDogEar = () => {
const dogEarType = getDogEarType({
isOwner,
Expand Down Expand Up @@ -298,13 +311,11 @@ export const DetailsTile = ({
text: styles.playButtonText,
icon: styles.playButtonIcon
}}
title={isPlaying ? messages.pause : messages.preview}
title={isPlayingPreview ? messages.pause : messages.preview}
size='large'
iconPosition='left'
icon={isPlaying ? IconPause : PlayIcon}
onPress={() => {
console.info('Preview button pressed')
}}
icon={isPlayingPreview ? IconPause : PlayIcon}
onPress={handlePressPreview}
disabled={!isPlayable}
fullWidth
/>
Expand Down Expand Up @@ -365,10 +376,10 @@ export const DetailsTile = ({
text: styles.playButtonText,
icon: styles.playButtonIcon
}}
title={isPlaying ? messages.pause : playText}
title={isPlayingFullAccess ? messages.pause : playText}
size='large'
iconPosition='left'
icon={isPlaying ? IconPause : PlayIcon}
icon={isPlayingFullAccess ? IconPause : PlayIcon}
onPress={handlePressPlay}
disabled={!isPlayable}
fullWidth
Expand All @@ -381,9 +392,7 @@ export const DetailsTile = ({
trackArtist={user}
/>
) : null}
{isUSDCPurchaseGated && (isOwner || !doesUserHaveAccess) ? (
<PreviewButton />
) : null}
{showPreviewButton ? <PreviewButton /> : null}
<DetailsTileActionButtons
hasReposted={!!hasReposted}
hasSaved={!!hasSaved}
Expand Down
7 changes: 7 additions & 0 deletions packages/mobile/src/components/details-tile/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type DetailsTileDetail = {
isHidden?: boolean
label: string
value: ReactNode

valueStyle?: TextStyle
}

Expand Down Expand Up @@ -64,6 +65,9 @@ export type DetailsTileProps = {
/** Is the item playing */
isPlaying?: boolean

/** Is the currently playing item a preview */
isPreviewing?: boolean

/** Is the item loaded and in a playable state */
isPlayable?: boolean

Expand All @@ -88,6 +92,9 @@ export type DetailsTileProps = {
/** Function to call when play button is pressed */
onPressPlay: GestureResponderHandler

/** Function to call when preview button is pressed */
onPressPreview?: GestureResponderHandler

/** Function to call when publish button is pressed */
onPressPublish?: GestureResponderHandler

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import { PlayBar } from './PlayBar'
import { TitleBar } from './TitleBar'
import { TrackInfo } from './TrackInfo'
import { PLAY_BAR_HEIGHT } from './constants'
import { useCurrentTrackDuration } from './useCurrentTrackDuration'
const { seek, reset } = playerActions

const { getPlaying, getCurrentTrack, getCounter, getUid } = playerSelectors
Expand Down Expand Up @@ -208,8 +209,8 @@ export const NowPlayingDrawer = memo(function NowPlayingDrawer(
const [isGestureEnabled, setIsGestureEnabled] = useState(true)

const track = useSelector(getCurrentTrack)
const trackDuration = useCurrentTrackDuration()
const trackId = track?.track_id
const trackDuration = track?.duration ?? 0

const user = useSelector((state) =>
getUser(state, track ? { id: track.owner_id } : {})
Expand Down Expand Up @@ -306,6 +307,7 @@ export const NowPlayingDrawer = memo(function NowPlayingDrawer(
<View style={styles.playBarContainer}>
<PlayBar
mediaKey={`${mediaKey}`}
duration={trackDuration}
track={track}
user={user}
onPress={onDrawerOpen}
Expand Down
5 changes: 3 additions & 2 deletions packages/mobile/src/components/now-playing-drawer/PlayBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,15 @@ const useStyles = makeStyles(({ palette, spacing }) => ({

type PlayBarProps = {
track: Nullable<Track>
duration: number
user: Nullable<User>
onPress: () => void
translationAnim: Animated.Value
mediaKey: string
}

export const PlayBar = (props: PlayBarProps) => {
const { track, user, onPress, translationAnim, mediaKey } = props
const { duration, track, user, onPress, translationAnim, mediaKey } = props
const styles = useStyles()
const dispatch = useDispatch()
const currentUser = useSelector(getAccountUser)
Expand Down Expand Up @@ -136,7 +137,7 @@ export const PlayBar = (props: PlayBarProps) => {
return (
<Animated.View style={[styles.root, { opacity: rootOpacityAnimation }]}>
<TrackingBar
duration={track?.duration ?? 0}
duration={duration}
mediaKey={mediaKey}
translateYAnimation={translationAnim}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { getTrackPreviewDuration, playerSelectors } from '@audius/common'
import { useSelector } from 'react-redux'

const { getCurrentTrack, getPreviewing } = playerSelectors

export const useCurrentTrackDuration = () => {
const track = useSelector(getCurrentTrack)
const isPreviewing = useSelector(getPreviewing)

return !track
? 0
: isPreviewing
? getTrackPreviewDuration(track)
: track.duration
}
Loading