Skip to content

Commit

Permalink
[C-2508] Update playback positions to be tracked per user id (#3374)
Browse files Browse the repository at this point in the history
  • Loading branch information
Kyle-Shanks authored May 24, 2023
1 parent 2d6b278 commit 16deebc
Show file tree
Hide file tree
Showing 21 changed files with 157 additions and 48 deletions.
46 changes: 40 additions & 6 deletions packages/common/src/store/playback-position/sagas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { call, delay, put, select, takeEvery } from 'typed-redux-saga'

import { AudioPlayer } from 'services/audio-player'
import { FeatureFlags } from 'services/remote-config'
import { getUserId } from 'store/account/selectors'
import { getTrack } from 'store/cache/tracks/selectors'
import { getContext } from 'store/effects'
import { getPlaying, getTrackId } from 'store/player/selectors'
Expand All @@ -13,7 +14,11 @@ import {
initializePlaybackPositionState,
setTrackPosition
} from './slice'
import { PlaybackPositionState, PLAYBACK_POSITION_LS_KEY } from './types'
import {
LEGACY_PLAYBACK_POSITION_LS_KEY,
PlaybackPositionState,
PLAYBACK_POSITION_LS_KEY
} from './types'

const RECORD_PLAYBACK_POSITION_INTERVAL = 1000

Expand All @@ -25,6 +30,8 @@ function* setInitialPlaybackPositionState() {
yield* call(remoteConfigInstance.waitForRemoteConfig)
const getFeatureEnabled = yield* getContext('getFeatureEnabled')
const getLocalStorageItem = yield* getContext('getLocalStorageItem')
const setLocalStorageItem = yield* getContext('setLocalStorageItem')
const removeLocalStorageItem = yield* getContext('removeLocalStorageItem')
const isNewPodcastControlsEnabled = yield* call(
getFeatureEnabled,
FeatureFlags.PODCAST_CONTROL_UPDATES_ENABLED,
Expand All @@ -36,11 +43,36 @@ function* setInitialPlaybackPositionState() {
getLocalStorageItem,
PLAYBACK_POSITION_LS_KEY
)
if (localStorageState === null) return
const legacyLocalStorageState = yield* call(
getLocalStorageItem,
LEGACY_PLAYBACK_POSITION_LS_KEY
)

const playbackPositionState: PlaybackPositionState =
JSON.parse(localStorageState)
yield* put(initializePlaybackPositionState({ playbackPositionState }))
if (localStorageState !== null) {
const playbackPositionState: PlaybackPositionState =
JSON.parse(localStorageState)
yield* put(initializePlaybackPositionState({ playbackPositionState }))
} else if (legacyLocalStorageState !== null) {
// NOTE: Check for legacy playback position state in local storage and update to new format
const userId = yield* select(getUserId)
if (!userId) return

const legacyPlaybackPositionState: PlaybackPositionState[number] =
JSON.parse(legacyLocalStorageState)
const convertedLegacyState: PlaybackPositionState = {
[userId]: legacyPlaybackPositionState
}
yield* put(
initializePlaybackPositionState({
playbackPositionState: convertedLegacyState
})
)
setLocalStorageItem(
PLAYBACK_POSITION_LS_KEY,
JSON.stringify(convertedLegacyState)
)
removeLocalStorageItem(LEGACY_PLAYBACK_POSITION_LS_KEY)
}
}

function* watchTrackPositionUpdate() {
Expand Down Expand Up @@ -84,15 +116,17 @@ function* savePlaybackPositionWorker() {
// eslint-disable-next-line no-unmodified-loop-condition
while (isNewPodcastControlsEnabled) {
const trackId = yield* select(getTrackId)
const userId = yield* select(getUserId)
const track = yield* select(getTrack, { id: trackId })
const playing = yield* select(getPlaying)
const isLongFormContent =
track?.genre === Genre.PODCASTS || track?.genre === Genre.AUDIOBOOKS

if (trackId && isLongFormContent && playing) {
if (userId && trackId && isLongFormContent && playing) {
const { position } = yield* call(getPlayerSeekInfo, audioPlayer)
yield* put(
setTrackPosition({
userId,
trackId,
positionInfo: {
status: 'IN_PROGRESS',
Expand Down
19 changes: 13 additions & 6 deletions packages/common/src/store/playback-position/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,22 @@ import { PlaybackPositionInfo } from './types'
export const getPlaybackPositions = (state: CommonState) =>
state.playbackPosition

export const getTrackPositions = (state: CommonState) =>
state.playbackPosition.trackPositions
export const getUserTrackPositions = (
state: CommonState,
props: { userId?: ID | null }
) => {
const { userId } = props
if (!userId) return null

return state.playbackPosition[userId]?.trackPositions ?? null
}

export const getTrackPosition = (
state: CommonState,
props: { trackId?: ID | null }
props: { userId?: ID | null; trackId?: ID | null }
): PlaybackPositionInfo | null => {
const { trackId } = props
if (!trackId) return null
const { userId, trackId } = props
if (!trackId || !userId) return null

return state.playbackPosition.trackPositions[trackId] ?? null
return state.playbackPosition[userId]?.trackPositions[trackId] ?? null
}
25 changes: 16 additions & 9 deletions packages/common/src/store/playback-position/slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@ type InitializePlaybackPositionStatePayload = {

type SetTrackPositionPayload = {
trackId: ID
userId?: ID | null
positionInfo: PlaybackPositionInfo
}

type ClearTrackPositionPayload = {
trackId: ID
userId?: ID | null
}

const initialState: PlaybackPositionState = {
trackPositions: {}
}
const initialState: PlaybackPositionState = {}

const slice = createSlice({
name: 'playback-position',
Expand All @@ -31,22 +31,29 @@ const slice = createSlice({
action: PayloadAction<InitializePlaybackPositionStatePayload>
) => {
const { playbackPositionState } = action.payload
state.trackPositions =
playbackPositionState.trackPositions ?? state.trackPositions
const userIds = Object.keys(playbackPositionState)
userIds.forEach((userId) => {
state[userId] = playbackPositionState[userId]
})
},
setTrackPosition: (
state,
action: PayloadAction<SetTrackPositionPayload>
) => {
const { trackId, positionInfo } = action.payload
state.trackPositions[trackId] = positionInfo
const { userId, trackId, positionInfo } = action.payload
if (!userId) return

const userState = state[userId] ?? { trackPositions: {} }
userState.trackPositions[trackId] = positionInfo
state[userId] = userState
},
clearTrackPosition: (
state,
action: PayloadAction<ClearTrackPositionPayload>
) => {
const { trackId } = action.payload
delete state.trackPositions[trackId]
const { userId, trackId } = action.payload
if (!userId) return
delete state[userId]?.trackPositions[trackId]
}
}
})
Expand Down
7 changes: 5 additions & 2 deletions packages/common/src/store/playback-position/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ID } from 'models/Identifiers'

export const PLAYBACK_POSITION_LS_KEY = 'playbackPosition'
export const LEGACY_PLAYBACK_POSITION_LS_KEY = 'playbackPosition'
export const PLAYBACK_POSITION_LS_KEY = 'userPlaybackPositions'

export type PlaybackStatus = 'IN_PROGRESS' | 'COMPLETED'

Expand All @@ -10,5 +11,7 @@ export type PlaybackPositionInfo = {
}

export type PlaybackPositionState = {
trackPositions: { [Key in ID]?: PlaybackPositionInfo }
[UserIdKey in ID]?: {
trackPositions: { [TrackIdKey in ID]?: PlaybackPositionInfo }
}
}
1 change: 1 addition & 0 deletions packages/common/src/store/storeContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { CommonState } from './reducers'
export type CommonStoreContext = {
getLocalStorageItem: (key: string) => Promise<string | null>
setLocalStorageItem: (key: string, value: string) => Promise<void>
removeLocalStorageItem: (key: string) => Promise<void>
getFeatureEnabled: (
flag: FeatureFlags,
fallbackFlag?: FeatureFlags
Expand Down
21 changes: 17 additions & 4 deletions packages/mobile/src/components/audio/Audio.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import { useRef, useEffect, useCallback, useState } from 'react'

import type { ID, Nullable, QueryParams, Track } from '@audius/common'
import type {
CommonState,
ID,
Nullable,
QueryParams,
Track
} from '@audius/common'
import {
getQueryParams,
removeNullable,
playbackRateValueMap,
accountSelectors,
cacheUsersSelectors,
cacheTracksSelectors,
playerSelectors,
Expand Down Expand Up @@ -59,12 +66,13 @@ import {
OfflineDownloadStatus
} from 'app/store/offline-downloads/slice'

const { getUserId } = accountSelectors
const { getUsers } = cacheUsersSelectors
const { getTracks } = cacheTracksSelectors
const { getPlaying, getSeek, getCurrentTrack, getCounter, getPlaybackRate } =
playerSelectors
const { setTrackPosition } = playbackPositionActions
const { getTrackPositions } = playbackPositionSelectors
const { getUserTrackPositions } = playbackPositionSelectors
const { recordListen } = tracksSocialActions
const {
getIndex,
Expand Down Expand Up @@ -153,7 +161,10 @@ export const Audio = () => {
const counter = useSelector(getCounter)
const repeatMode = useSelector(getRepeat)
const playbackRate = useSelector(getPlaybackRate)
const trackPositions = useSelector(getTrackPositions)
const currentUserId = useSelector(getUserId)
const trackPositions = useSelector((state: CommonState) =>
getUserTrackPositions(state, { userId: currentUserId })
)

const isReachable = useSelector(getIsReachable)
const isNotReachable = isReachable === false
Expand Down Expand Up @@ -386,14 +397,15 @@ export const Audio = () => {
const isLongFormContent =
track?.genre === Genre.PODCASTS ||
track?.genre === Genre.AUDIOBOOKS
const trackPosition = trackPositions[track.track_id]
const trackPosition = trackPositions?.[track.track_id]
if (trackPosition?.status === 'IN_PROGRESS') {
dispatch(
playerActions.seek({ seconds: trackPosition.playbackPosition })
)
} else if (isNewPodcastControlsEnabled && isLongFormContent) {
dispatch(
setTrackPosition({
userId: currentUserId,
trackId: track.track_id,
positionInfo: {
status: 'IN_PROGRESS',
Expand Down Expand Up @@ -435,6 +447,7 @@ export const Audio = () => {
if (isLongFormContent && isAtEndOfTrack) {
dispatch(
setTrackPosition({
userId: currentUserId,
trackId: track.track_id,
positionInfo: {
status: 'COMPLETED',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { SearchTrack, Track } from '@audius/common'
import {
accountSelectors,
formatLineupTileDuration,
playbackPositionSelectors
} from '@audius/common'
Expand All @@ -13,6 +14,7 @@ import { useThemeColors } from 'app/utils/theme'

import { ProgressBar } from '../progress-bar'

const { getUserId } = accountSelectors
const { getTrackPosition } = playbackPositionSelectors

const messages = {
Expand Down Expand Up @@ -64,8 +66,9 @@ export const DetailsProgressInfo = ({ track }: DetailsProgressInfoProps) => {
const { duration } = track
const { neutralLight4 } = useThemeColors()
const styles = useStyles()
const currentUserId = useSelector(getUserId)
const playbackPositionInfo = useSelector((state) =>
getTrackPosition(state, { trackId: track.track_id })
getTrackPosition(state, { trackId: track.track_id, userId: currentUserId })
)

const progressText = playbackPositionInfo
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ export const DetailsTile = ({
)

const playbackPositionInfo = useSelector((state) =>
getTrackPosition(state, { trackId })
getTrackPosition(state, { trackId, userId: currentUserId })
)

const playText =
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { PremiumConditions, Nullable } from '@audius/common'
import {
FeatureFlags,
accountSelectors,
playbackPositionSelectors,
formatLineupTileDuration
} from '@audius/common'
Expand All @@ -25,6 +26,7 @@ import { ProgressBar } from '../progress-bar'

import { useStyles as useTrackTileStyles } from './styles'

const { getUserId } = accountSelectors
const { getTrackPosition } = playbackPositionSelectors

const messages = {
Expand Down Expand Up @@ -139,8 +141,9 @@ export const LineupTileTopRight = ({
const { neutralLight4, secondary } = useThemeColors()
const accentBlue = useColor('accentBlue')
const trackTileStyles = useTrackTileStyles()
const currentUserId = useSelector(getUserId)
const playbackPositionInfo = useSelector((state) =>
getTrackPosition(state, { trackId })
getTrackPosition(state, { trackId, userId: currentUserId })
)

const isInProgress =
Expand Down
2 changes: 1 addition & 1 deletion packages/mobile/src/components/lineup-tile/TrackTile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ export const TrackTileComponent = ({
}, [navigation, track_id])

const playbackPositionInfo = useSelector((state) =>
getTrackPosition(state, { trackId: track_id })
getTrackPosition(state, { trackId: track_id, userId: currentUserId })
)
const handlePressOverflow = useCallback(() => {
if (track_id === undefined) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,10 @@ export const ActionsBar = ({ track }: ActionsBarProps) => {
}, [dispatch, track])

const playbackPositionInfo = useSelector((state) =>
getTrackPosition(state, { trackId: track?.track_id })
getTrackPosition(state, {
trackId: track?.track_id,
userId: accountUser?.user_id
})
)
const onPressOverflow = useCallback(() => {
if (track) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
FollowSource,
RepostSource,
ShareSource,
accountSelectors,
cacheTracksSelectors,
cacheUsersSelectors,
tracksSocialActions,
Expand All @@ -23,6 +24,7 @@ import { useToast } from 'app/hooks/useToast'
import { AppTabNavigationContext } from 'app/screens/app-screen'
import { setVisibility } from 'app/store/drawers/slice'

const { getUserId } = accountSelectors
const { getMobileOverflowModal } = mobileOverflowMenuUISelectors
const { requestOpen: openAddToPlaylistModal } = addToPlaylistUIActions
const { followUser, unfollowUser } = usersSocialActions
Expand All @@ -44,6 +46,7 @@ const messages = {
const TrackOverflowMenuDrawer = ({ render }: Props) => {
const { onClose: closeNowPlayingDrawer } = useDrawer('NowPlaying')
const { navigation: contextNavigation } = useContext(AppTabNavigationContext)
const currentUserId = useSelector(getUserId)
const navigation = useNavigation({ customNavigation: contextNavigation })
const dispatch = useDispatch()
const { toast } = useToast()
Expand Down Expand Up @@ -110,14 +113,15 @@ const TrackOverflowMenuDrawer = ({ render }: Props) => {
[OverflowAction.MARK_AS_PLAYED]: () => {
dispatch(
setTrackPosition({
userId: currentUserId,
trackId: id,
positionInfo: { status: 'COMPLETED', playbackPosition: 0 }
})
)
toast({ content: messages.markedAsPlayed })
},
[OverflowAction.MARK_AS_UNPLAYED]: () => {
dispatch(clearTrackPosition({ trackId: id }))
dispatch(clearTrackPosition({ trackId: id, userId: currentUserId }))
toast({ content: messages.markedAsUnplayed })
}
}
Expand Down
Loading

0 comments on commit 16deebc

Please sign in to comment.