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

Commit

Permalink
Support /playlists/{hashid}
Browse files Browse the repository at this point in the history
  • Loading branch information
raymondjacobson committed Aug 27, 2020
1 parent cb9596a commit 4b764f5
Show file tree
Hide file tree
Showing 6 changed files with 174 additions and 59 deletions.
4 changes: 3 additions & 1 deletion src/containers/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ import {
TRENDING_GENRES,
APP_REDIRECT,
TRACK_ID_PAGE,
USER_ID_PAGE
USER_ID_PAGE,
PLAYLIST_ID_PAGE
} from 'utils/route'
import 'utils/redirect'
import { isMobile, getClient } from 'utils/clientUtil'
Expand Down Expand Up @@ -718,6 +719,7 @@ class App extends Component {
)}
/>
<Route exact path={TRACK_ID_PAGE} component={TrackPage} />
<Route exact path={PLAYLIST_ID_PAGE} component={CollectionPage} />

<Route exact path={TRACK_PAGE} component={TrackPage} />

Expand Down
84 changes: 35 additions & 49 deletions src/containers/collection-page/CollectionPageProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { connect } from 'react-redux'
import { withRouter, RouteComponentProps } from 'react-router-dom'
import { UnregisterCallback } from 'history'
import { push as pushRoute, replace } from 'connected-react-router'
import { matchPath } from 'react-router'
import { AppState, Status, Kind } from 'store/types'
import { Dispatch } from 'redux'

Expand All @@ -26,7 +25,9 @@ import {
FEED_PAGE,
REPOSTING_USERS_ROUTE,
FAVORITING_USERS_ROUTE,
fullPlaylistPage
fullPlaylistPage,
playlistPage,
albumPage
} from 'utils/route'
import { setRepost } from 'containers/reposts-page/store/actions'
import { RepostType } from 'containers/reposts-page/store/types'
Expand Down Expand Up @@ -77,6 +78,7 @@ import {
} from 'store/application/ui/userListModal/types'
import { SmartCollection } from 'models/Collection'
import DeletedPage from 'containers/deleted-page/DeletedPage'
import { parseCollectionRoute } from 'utils/route/collectionRouteParser'

type OwnProps = {
type: CollectionsPageType
Expand Down Expand Up @@ -143,7 +145,7 @@ class CollectionPage extends Component<

componentDidUpdate(prevProps: CollectionPageProps) {
const {
collection: { userUid, metadata, status },
collection: { userUid, metadata, status, user },
smartCollection,
tracks,
location: { pathname },
Expand All @@ -156,12 +158,12 @@ class CollectionPage extends Component<

const { updatingRoute, initialOrder } = this.state

const routeParams = this.getRouteParams(pathname)
if (!routeParams) return
const params = parseCollectionRoute(pathname)
if (!params) return
if (status === Status.ERROR) {
if (
routeParams &&
routeParams.id === this.state.playlistId &&
params &&
params.collectionId === this.state.playlistId &&
metadata?.playlist_owner_id !== this.props.userId
) {
// Only route to not found page if still on the collection page and
Expand Down Expand Up @@ -215,17 +217,25 @@ class CollectionPage extends Component<
const {
collection: { metadata: prevMetadata }
} = prevProps
if (
metadata &&
prevMetadata &&
metadata.playlist_name !== prevMetadata.playlist_name
) {
const routeParams = this.getRouteParams(pathname)
if (routeParams) {
const { id, name } = routeParams
if (metadata) {
const params = parseCollectionRoute(pathname)
if (params) {
const { collectionId, title, collectionType, handle } = params
const newCollectionName = formatUrlName(metadata.playlist_name)
if (newCollectionName !== name && id === metadata.playlist_id) {
const newPath = pathname.replace(name, newCollectionName)

if ((!title || !handle || !collectionType) && user) {
const newPath = metadata.is_album
? albumPage(user.handle, metadata.playlist_name, collectionId)
: playlistPage(user.handle, metadata.playlist_name, collectionId)
this.props.replaceRoute(newPath)
} else if (
prevMetadata &&
metadata.playlist_name !== prevMetadata.playlist_name &&
title &&
newCollectionName !== title &&
collectionId === metadata.playlist_id
) {
const newPath = pathname.replace(title, newCollectionName)
this.props.replaceRoute(newPath)
}
}
Expand Down Expand Up @@ -270,16 +280,17 @@ class CollectionPage extends Component<
}

fetchCollection = (pathname: string, forceFetch = false) => {
const routeParams = this.getRouteParams(pathname)
if (routeParams) {
const { id, handle } = routeParams
if (forceFetch || id !== this.state.playlistId) {
const params = parseCollectionRoute(pathname)
if (params) {
const { handle, collectionId } = params
if (forceFetch || collectionId !== this.state.playlistId) {
this.resetCollection()
this.setState({ playlistId: id as number })
this.props.fetchCollection(handle, id as number)
this.setState({ playlistId: collectionId as number })
this.props.fetchCollection(handle, collectionId as number)
this.props.fetchTracks()
}
}

if (
this.props.smartCollection &&
this.props.smartCollection.playlist_contents
Expand All @@ -288,31 +299,6 @@ class CollectionPage extends Component<
}
}

getRouteParams = (pathname: string) => {
const match = matchPath<{
handle: string
collectionType: string
name: string
}>(pathname, {
path: '/:handle/:collectionType/:name',
exact: true
})
if (
!match ||
(match.params.collectionType !== 'playlist' &&
match.params.collectionType !== 'album')
) {
return null
}
const collectionType = match.params.collectionType
const nameParts = match.params.name.split('-')
const handleEncoded = match.params.handle
const handle = decodeURIComponent(handleEncoded)
const name = nameParts.slice(0, -1).join('-')
const id = this.maybeParseInt(nameParts[nameParts.length - 1])
return { id, name, handle, collectionType }
}

resetCollection = () => {
const {
collection: { collectionUid, userUid }
Expand Down Expand Up @@ -806,7 +792,7 @@ function makeMapStateToProps() {

function mapDispatchToProps(dispatch: Dispatch) {
return {
fetchCollection: (handle: string, id: number) =>
fetchCollection: (handle: string | null, id: number) =>
dispatch(collectionActions.fetchCollection(handle, id)),
fetchTracks: () =>
dispatch(tracksActions.fetchLineupMetadatas(0, 200, false, undefined)),
Expand Down
19 changes: 12 additions & 7 deletions src/containers/collection-page/store/sagas.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,28 +15,33 @@ function* watchFetchCollection() {
const collectionId = action.id
const handle = action.handle

const user = yield call(fetchUserByHandle, handle)
if (!user) {
yield put(collectionActions.fetchCollectionFailed())
let user
if (handle) {
user = yield call(fetchUserByHandle, handle)
if (!user) {
yield put(collectionActions.fetchCollectionFailed())
}
}
const userUid = makeUid(Kind.USERS, user.user_id)

// Retrieve collections and fetch nested tracks
const { collections, uids: collectionUids } = yield call(
retrieveCollections,
user.user_id,
user?.user_id ?? null,
[collectionId],
true
)

if (Object.values(collections).length === 0) {
yield put(collectionActions.fetchCollectionFailed(userUid))
yield put(collectionActions.fetchCollectionFailed())
}
const collection = collections[collectionId]
const userUid = makeUid(Kind.USERS, collection.playlist_owner_id)
const collectionUid = collectionUids[collectionId]
if (collection) {
yield put(
cacheActions.subscribe(Kind.USERS, [{ uid: userUid, id: user.user_id }])
cacheActions.subscribe(Kind.USERS, [
{ uid: userUid, id: collection.playlist_owner_id }
])
)
yield put(
collectionActions.fetchCollectionSucceeded(
Expand Down
3 changes: 1 addition & 2 deletions src/containers/track-page/TrackPageProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -150,9 +150,9 @@ class TrackPageProvider extends Component<
const params = parseTrackRoute(pathname)
if (params) {
const { trackTitle, trackId, handle } = params
const newTrackTitle = formatUrlName(track.title)
if (!trackTitle || !handle) {
if (this.props.user) {
const newTrackTitle = formatUrlName(track.title)
const newPath = trackPage(
this.props.user.handle,
newTrackTitle,
Expand All @@ -161,7 +161,6 @@ class TrackPageProvider extends Component<
this.props.replaceRoute(newPath)
}
} else {
const newTrackTitle = formatUrlName(track.title)
if (track.track_id === trackId) {
if (newTrackTitle !== trackTitle) {
const newPath = pathname.replace(trackTitle, newTrackTitle)
Expand Down
69 changes: 69 additions & 0 deletions src/utils/route/collectionRouteParser.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { parseCollectionRoute } from './collectionRouteParser'
// eslint-disable-next-line
import { mockDecode } from '__mocks__/Hashids'

describe('parseCollectionRoute', () => {
it('can decode a playlist id route', () => {
const route = '/arizmendi/playlist/croissants-11'
const {
title,
collectionId,
handle,
collectionType
} = parseCollectionRoute(route)
expect(title).toEqual('croissants')
expect(collectionId).toEqual(11)
expect(handle).toEqual('arizmendi')
expect(collectionType).toEqual('playlist')
})

it('can decode an album id route', () => {
const route = '/arizmendi/album/scones-20'
const {
title,
collectionId,
handle,
collectionType
} = parseCollectionRoute(route)
expect(title).toEqual('scones')
expect(collectionId).toEqual(20)
expect(handle).toEqual('arizmendi')
expect(collectionType).toEqual('album')
})

it('can decode a hashed collection id route', () => {
mockDecode.mockReturnValue([11845])

const route = '/playlists/eP9k7'
const {
title,
collectionId,
handle,
collectionType
} = parseCollectionRoute(route)
expect(title).toEqual(null)
expect(collectionId).toEqual(11845)
expect(handle).toEqual(null)
expect(collectionType).toEqual(null)
})

it('returns null for invalid id in playlist id route', () => {
const route = '/arizmendi/playlist/name-asdf'
const params = parseCollectionRoute(route)
expect(params).toEqual(null)
})

it('returns null for invalid id in album id route', () => {
const route = '/arizmendi/album/name-asdf'
const params = parseCollectionRoute(route)
expect(params).toEqual(null)
})

it('returns null for invalid id in hashed collection id route', () => {
mockDecode.mockReturnValue([NaN])

const route = '/playlists/asdf'
const params = parseCollectionRoute(route)
expect(params).toEqual(null)
})
})
54 changes: 54 additions & 0 deletions src/utils/route/collectionRouteParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { matchPath } from 'react-router-dom'
import { PLAYLIST_PAGE, ALBUM_PAGE, PLAYLIST_ID_PAGE } from 'utils/route'
import { decodeHashId } from './hashIds'

/**
* Parses a collection route into handle, title, id, and type
* If the route is a hash id route, title, handle, and type are not returned
* @param route
*/
export const parseCollectionRoute = (route: string) => {
const collectionIdPageMatch = matchPath<{ id: string }>(route, {
path: PLAYLIST_ID_PAGE,
exact: true
})
if (collectionIdPageMatch) {
const collectionId = decodeHashId(collectionIdPageMatch.params.id)
if (!collectionId || isNaN(collectionId)) return null
return { collectionId, handle: null, collectionType: null, title: null }
}

const playlistPageMatch = matchPath<{
handle: string
playlistName: string
}>(route, {
path: PLAYLIST_PAGE,
exact: true
})
if (playlistPageMatch) {
const { handle, playlistName } = playlistPageMatch.params
const nameParts = playlistName.split('-')
const title = nameParts.slice(0, nameParts.length - 1).join('-')
const collectionId = parseInt(nameParts[nameParts.length - 1], 10)
if (!collectionId || isNaN(collectionId)) return null
return { title, collectionId, handle, collectionType: 'playlist' }
}

const albumPageMatch = matchPath<{
handle: string
albumName: string
}>(route, {
path: ALBUM_PAGE,
exact: true
})
if (albumPageMatch) {
const { handle, albumName } = albumPageMatch.params
const nameParts = albumName.split('-')
const title = nameParts.slice(0, nameParts.length - 1).join('-')
const collectionId = parseInt(nameParts[nameParts.length - 1], 10)
if (!collectionId || isNaN(collectionId)) return null
return { title, collectionId, handle, collectionType: 'album' }
}

return null
}

0 comments on commit 4b764f5

Please sign in to comment.