Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
21 changes: 14 additions & 7 deletions packages/common/src/api/tan-query/lineups/useWeeklyRotation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,25 @@ const STALE_TIME_MS = 30 * 60 * 1000

export type UseWeeklyRotationArgs = {
limit?: number
/**
* Whose mix. Defaults to the signed-in user; pass another user's id to
* view a shared mix. The endpoint is public, so any user works.
*/
userId?: ID | null
}

export const getWeeklyRotationQueryKey = ({
userId,
limit = DEFAULT_LIMIT
}: UseWeeklyRotationArgs & { userId: ID | null | undefined }) =>
}: Omit<UseWeeklyRotationArgs, 'userId'> & { userId: ID | null | undefined }) =>
[QUERY_KEYS.weeklyRotation, userId, { limit }] as unknown as QueryKey<
LineupData[]
>

/**
* The current user's Weekly Rotation mix: tracks they haven't heard, weighted
* toward artists they don't already follow.
* A user's Weekly Rotation mix: tracks they haven't heard, weighted toward
* artists they don't already follow. The current user's by default; a shared
* link passes the sharer's id.
*
* Deliberately a plain `useQuery` rather than an infinite one — the mix is a
* fixed-size artifact, not a lineup you scroll. There is no page 2.
Expand All @@ -44,19 +50,20 @@ export const getWeeklyRotationQueryKey = ({
* the request count low while still letting a bad result heal.
*/
export const useWeeklyRotation = (
{ limit = DEFAULT_LIMIT }: UseWeeklyRotationArgs = {},
{ limit = DEFAULT_LIMIT, userId: userIdArg }: UseWeeklyRotationArgs = {},
options?: QueryOptions
) => {
const { audiusSdk } = useQueryContext()
const { data: currentUserId } = useCurrentUserId()
const queryClient = useQueryClient()
const userId = userIdArg ?? currentUserId

const query = useQuery({
queryKey: getWeeklyRotationQueryKey({ userId: currentUserId, limit }),
queryKey: getWeeklyRotationQueryKey({ userId, limit }),
queryFn: async () => {
const sdk = await audiusSdk()
const { data = [] } = await sdk.users.getWeeklyRotation({
id: Id.parse(currentUserId),
id: Id.parse(userId),
limit,
userId: OptionalId.parse(currentUserId)
})
Expand All @@ -69,7 +76,7 @@ export const useWeeklyRotation = (
},
staleTime: STALE_TIME_MS,
...options,
enabled: options?.enabled !== false && !!currentUserId
enabled: options?.enabled !== false && !!userId
})

const data = query.data ?? []
Expand Down
8 changes: 8 additions & 0 deletions packages/common/src/hooks/useShareContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@ export const useShareContent = (
const profileId = request?.type === 'profile' ? request.profileId : null
const collectionId =
request?.type === 'collection' ? request.collectionId : null
const weeklyRotationUserId =
request?.type === 'weeklyRotation' ? request.userId : null

const { data: track } = useTrack(trackId)
const { data: profile } = useUser(profileId)
const { data: collection } = useCollection(collectionId)
const { data: weeklyRotationUser } = useUser(weeklyRotationUserId)

const trackArtistId = track?.owner_id ?? null
const collectionOwnerId = collection?.playlist_owner_id ?? null
Expand Down Expand Up @@ -46,5 +49,10 @@ export const useShareContent = (
return { type: 'playlist', playlist: collection, creator: collectionOwner }
}

if (request.type === 'weeklyRotation') {
if (!weeklyRotationUser) return null
return { type: 'weeklyRotation', user: weeklyRotationUser }
}

return null
}
5 changes: 3 additions & 2 deletions packages/common/src/messages/explore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,11 @@ export const exploreMessages = {
imFeelingLucky: "I'm Feeling Lucky",
recentlyPlayed: 'Recently Played',
weeklyRotation: 'Your Weekly Rotation',
weeklyRotationSubtitle: 'Updated every Monday',
weeklyRotationFor: (name: string) => `${name}'s Weekly Rotation`,
weeklyRotationSubtitle: 'Updated every Wednesday',
weeklyRotationBadge: 'New',
weeklyRotationPitch:
'A fresh mix of tracks picked just for you. Updated every Monday.',
'A fresh mix of tracks picked just for you. Updated every Wednesday.',
weeklyRotationCta: 'Listen Now',
weeklyRotationTrackCount: (count: number) =>
`${count} ${count === 1 ? 'track' : 'tracks'}`,
Expand Down
4 changes: 2 additions & 2 deletions packages/common/src/models/Analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -719,15 +719,15 @@ export enum FollowSource {

type Share = {
eventName: Name.SHARE
kind: 'profile' | 'album' | 'playlist' | 'track'
kind: 'profile' | 'album' | 'playlist' | 'track' | 'weeklyRotation'
source: ShareSource
id: string
url: string
}

export type ShareToTwitter = {
eventName: Name.SHARE_TO_TWITTER
kind: 'profile' | 'album' | 'playlist' | 'track'
kind: 'profile' | 'album' | 'playlist' | 'track' | 'weeklyRotation'
source: ShareSource
id: number
url: string
Expand Down
21 changes: 20 additions & 1 deletion packages/common/src/store/ui/share-modal/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ import { Nullable } from '~/utils/typeUtils'

import { ID, ShareSource, Collection, Track, User } from '../../../models'

export type ShareType = 'track' | 'profile' | 'album' | 'playlist' | 'contest'
export type ShareType =
| 'track'
| 'profile'
| 'album'
| 'playlist'
| 'contest'
| 'weeklyRotation'

type ShareTrackContent = {
type: 'track'
Expand Down Expand Up @@ -46,18 +52,31 @@ type SharePlaylistContent = {
creator: User
}

/**
* A listener's Weekly Rotation mix. There is no entity behind it -- the mix
* is computed on demand from the listener's id -- so the only thing the share
* needs is the listener: the link is built from their handle and the card
* from their current mix.
*/
type ShareWeeklyRotationContent = {
type: 'weeklyRotation'
user: User
}

export type ShareContent =
| ShareTrackContent
| ShareContestContent
| ShareProfileContent
| ShareAlbumContent
| SharePlaylistContent
| ShareWeeklyRotationContent

export type ShareModalRequest =
| { type: 'track'; trackId: ID }
| { type: 'contest'; trackId: ID }
| { type: 'profile'; profileId: ID }
| { type: 'collection'; collectionId: ID }
| { type: 'weeklyRotation'; userId: ID }

export type ShareModalState = {
source: Nullable<ShareSource>
Expand Down
8 changes: 8 additions & 0 deletions packages/common/src/utils/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ export const EXPLORE_PAGE = '/explore'
export const TRENDING_PLAYLISTS_PAGE = '/explore/playlists'
export const TRENDING_UNDERGROUND_PAGE = '/explore/underground'
export const WEEKLY_ROTATION_PAGE = '/explore/weekly-rotation'
// Someone else's mix. Lives under /explore rather than /:handle/weekly-rotation
// because /:handle/:slug is the track permalink pattern.
export const WEEKLY_ROTATION_USER_PAGE = '/explore/weekly-rotation/:handle'
export const CONTESTS_PAGE = '/contests'

// DEPRECATED - use /library instead.
Expand Down Expand Up @@ -303,6 +306,7 @@ export const orderedRoutes = [
TRENDING_PAGE,
EXPLORE_PAGE,
WEEKLY_ROTATION_PAGE,
WEEKLY_ROTATION_USER_PAGE,
CONTESTS_PAGE,
EMPTY_PAGE,
SEARCH_PAGE,
Expand Down Expand Up @@ -427,6 +431,10 @@ export const staticRoutes = new Set([
CHATS_PAGE
])

export const weeklyRotationPage = (handle: string) => {
return `${WEEKLY_ROTATION_PAGE}/${encodeUrlName(handle)}`
}

export const profilePage = (handle: string | null | undefined) => {
return `/${encodeUrlName(handle ?? '')}`
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ const NavigationContainer = (props: NavigationContainerProps) => {
MostLoved: 'explore/most-loved',
FeelingLucky: 'explore/feeling-lucky',
HeavyRotation: 'explore/heavy-rotation',
WeeklyRotationScreen:
'explore/weekly-rotation/:handle?',
ChillPlaylists: 'explore/chill',
IntensePlaylists: 'explore/intense',
IntimatePlaylists: 'explore/intimate',
Expand Down
31 changes: 30 additions & 1 deletion packages/mobile/src/components/share-drawer/ShareDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useRef } from 'react'

import { useCurrentUserId } from '@audius/common/api'
import { useShareAction, useShareContent } from '@audius/common/hooks'
import { exploreMessages } from '@audius/common/messages'
import { Name, ShareSource } from '@audius/common/models'
import { registerNiceModalId } from '@audius/common/services'
import {
Expand Down Expand Up @@ -31,6 +32,8 @@ import { useToast } from 'app/hooks/useToast'
import type { AppTabScreenParamList } from 'app/screens/app-screen'
import { make, track } from 'app/services/analytics'
import { makeStyles } from 'app/styles'
import { getWeeklyRotationRoute } from 'app/utils/routes'
import share from 'app/utils/share'
import { useThemeColors } from 'app/utils/theme'

import { ActionDrawerWithoutRedux } from '../action-drawer/ActionDrawerWithoutRedux'
Expand Down Expand Up @@ -157,6 +160,27 @@ export const ShareDrawer = NiceModal.create(() => {
case 'playlist':
dispatch(shareCollection(content.playlist.playlist_id, source))
break
case 'weeklyRotation': {
// No entity behind the mix, so no social saga: open the system
// sheet directly with the same link Copy Link uses, and emit the
// same Share event the sagas do.
// `share` prepends AUDIUS_URL itself, so hand it the path.
const url = getWeeklyRotationRoute(content.user)
share({
url,
message: exploreMessages.weeklyRotationFor(content.user.name)
})
track(
make({
eventName: Name.SHARE,
kind: 'weeklyRotation',
id: `${content.user.user_id}`,
url,
source
})
)
break
}
}
}, [dispatch, content, source])

Expand Down Expand Up @@ -232,6 +256,10 @@ export const ShareDrawer = NiceModal.create(() => {
result.push(shareToInstagramStoriesAction)
result.push(shareVideoToTiktokAction)
result.push(shareToSnapchatAction)
} else if (content?.type === 'weeklyRotation') {
// The story/video paths need a single streamable track; a mix has
// none, but X works off the link alone.
result.push(shareToXAction)
}

result.push(copyLinkAction, shareSheetAction)
Expand All @@ -247,7 +275,8 @@ export const ShareDrawer = NiceModal.create(() => {
handleOpenShareSheet,
handleShareToSnapchat,
handleShareToInstagramStory,
isShareableTrack
isShareableTrack,
content?.type
])

// Trigger share action on mount with new content
Expand Down
5 changes: 4 additions & 1 deletion packages/mobile/src/components/share-drawer/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ const shareTypeMap: Record<ShareType, string> = {
profile: 'Profile',
album: 'Album',
playlist: 'Playlist',
contest: 'Contest'
contest: 'Contest',
weeklyRotation: 'Weekly Rotation'
}

export const messages = {
Expand All @@ -30,6 +31,8 @@ export const messages = {
`Check out ${albumName} by ${handle} @audius $AUDIO`,
playlistShareText: (playlistName: string, handle: string) =>
`Check out ${playlistName} by ${handle} @audius $AUDIO`,
weeklyRotationShareText: (handle: string) =>
`Check out ${handle}'s Weekly Rotation on @audius $AUDIO`,
loadingStoryModalTitle: 'Generating Story',
loadingInstagramStorySubtitle: 'Preparing to open Instagram',
loadingSnapchatSubtitle: 'Preparing to open Snapchat',
Expand Down
11 changes: 10 additions & 1 deletion packages/mobile/src/components/share-drawer/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import {
getCollectionRoute,
getContestRoute,
getTrackRoute,
getUserRoute
getUserRoute,
getWeeklyRotationRoute
} from 'app/utils/routes'

import { messages } from './messages'
Expand Down Expand Up @@ -35,6 +36,10 @@ export const getContentUrl = (content: ShareContent) => {
const { playlist } = content
return getCollectionRoute(playlist, true)
}
case 'weeklyRotation': {
const { user } = content
return getWeeklyRotationRoute(user, true)
}
}
}

Expand Down Expand Up @@ -72,6 +77,10 @@ export const getXShareText = async (content: ShareContent) => {
} = content
return messages.playlistShareText(playlist_name, getXShareHandle(creator))
}
case 'weeklyRotation': {
const { user } = content
return messages.weeklyRotationShareText(getXShareHandle(user))
}
}
}

Expand Down
3 changes: 2 additions & 1 deletion packages/mobile/src/screens/app-screen/AppTabScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ export type AppTabScreenParamList = {
SettingsScreen: undefined
AboutScreen: undefined
ListeningHistoryScreen: undefined
WeeklyRotationScreen: undefined
// No handle: the signed-in user's own mix. With one: a shared mix.
WeeklyRotationScreen: { handle?: string } | undefined
AccountSettingsScreen: undefined
ChangeEmail: undefined
ChangePassword: undefined
Expand Down
Loading
Loading