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
1 change: 1 addition & 0 deletions packages/common/src/messages/remixes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export const remixMessages = {
remixesTitle: 'Remixes',
remixes: 'Remix',
submissionsTitle: 'Remix Contest Submissions',
pickWinnersSubmissionsTitle: 'Submissions',
submissions: 'Submission',
coSigned: 'Co-Signs',
contestEntries: 'Contest Entries',
Expand Down
41 changes: 23 additions & 18 deletions packages/mobile/src/screens/contest-screen/ContestStemsCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,20 @@ export const ContestStemsCard = ({ trackId }: ContestStemsCardProps) => {
{ enabled: stems.length > 0 || !!track?.is_downloadable }
)

// Default to expanded so the stems list is visible without an extra
// tap. Once we support multiple source tracks per contest we'll flip
// back to collapsed-by-default to keep the surface compact. Matches
// the web ContestStemsCard.
const [expanded, setExpanded] = useState(true)
// Default to expanded for short lists so the user sees the stems
// without an extra tap; auto-collapse when there are more than
// STEMS_COLLAPSE_THRESHOLD entries so a long list doesn't push the
// comments tab way down the screen. Explicit user toggle wins.
const STEMS_COLLAPSE_THRESHOLD = 5
const stemsBelowThreshold = stemsCount <= STEMS_COLLAPSE_THRESHOLD
const [expandedOverride, setExpandedOverride] = useState<boolean | null>(null)
const expanded = expandedOverride ?? stemsBelowThreshold
const setExpanded = (next: boolean | ((prev: boolean) => boolean)) => {
setExpandedOverride((prev) => {
const current = prev ?? stemsBelowThreshold
return typeof next === 'function' ? next(current) : next
})
}

const { onOpen: openWaitForDownloadModal } = useWaitForDownloadModal()

Expand Down Expand Up @@ -146,19 +155,15 @@ export const ContestStemsCard = ({ trackId }: ContestStemsCardProps) => {
if (!track || !artist) return null

return (
<Paper direction='column' borderRadius='m' shadow='flat'>
{/* Heading sits in the top padding slot — matches the web
ContestStemsCard. The inner padding+border wrapper that used
to live here was dropped per the contest QA pass: web has no
inner container, so the mobile card now reads as one surface
with the same vertical rhythm. */}
<View style={{ paddingTop: 16, paddingHorizontal: 16 }}>
<Text variant='label' size='m' color='subdued'>
{messages.heading}
</Text>
</View>
<Paper direction='column' p='l' gap='m' borderRadius='m' shadow='flat'>
<Text variant='label' size='m' color='subdued'>
{messages.heading}
</Text>

<Flex direction='column'>
{/* Inner bordered container — separates the source-track +
stems block from the card heading and any future siblings,
matching Figma 2925-18101. */}
<Paper direction='column' borderRadius='m' shadow='flat' border='default'>
{/* Collapsed summary row: artwork + access label + artist +
caret. */}
<Flex direction='row' p='l' gap='m' alignItems='center'>
Expand Down Expand Up @@ -260,7 +265,7 @@ export const ContestStemsCard = ({ trackId }: ContestStemsCardProps) => {
))}
</View>
) : null}
</Flex>
</Paper>
</Paper>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import React, { useMemo } from 'react'
import React from 'react'

import { useExploreContent, useTracks } from '@audius/common/api'
import {
useAllRemixContests,
useExploreContent,
useTracks
} from '@audius/common/api'
import { useFeatureFlag } from '@audius/common/hooks'
import { exploreMessages as messages } from '@audius/common/messages'
import { FeatureFlags } from '@audius/common/services'
Expand All @@ -22,30 +26,25 @@ export const FeaturedRemixContests = () => {
FeatureFlags.CONTESTS
)

// When the contests feature is on the section title is just
// "Contests" — switch the data source from the curated featured
// list to the full live list so the carousel actually matches the
// label (Julian: "the full list of contests isn't showing on the
// mobile discovery page"). Backed by `useAllRemixContests`, the same
// source the dedicated /contests screen uses.
const { data: allContestTrackIds, isPending: isAllContestsPending } =
useAllRemixContests(undefined, { enabled: inView && isContestsPageEnabled })

const { data: exploreContent, isPending: isExplorePending } =
useExploreContent({ enabled: inView })
useExploreContent({ enabled: inView && !isContestsPageEnabled })

// The curated featured-contests list is a static JSON file (see
// useExploreContent). Some entries can refer to tracks the artist has
// since deleted or made private — when ContestCard hits one of those it
// renders `null`, but CardList's row wrapper (a fixed-width View) keeps
// the card-sized slot, leaving a visible gap in the carousel. Hydrating
// the tracks here lets us filter the list down to ones a card will
// actually render for.
const { data: remixContests, isPending: isTracksPending } = useTracks(
// Old-card path needs the hydrated track list; new-card path resolves per
// card internally so we skip this fetch when the flag is enabled.
const { data: remixContests } = useTracks(
exploreContent?.featuredRemixContests,
{ enabled: inView }
{ enabled: inView && !isContestsPageEnabled }
)

const validTrackIds = useMemo(() => {
if (!remixContests) return undefined
return remixContests
.filter((t) => !t.is_delete && !t.is_unlisted)
.map((t) => t.track_id)
}, [remixContests])

const isLoading = isExplorePending || isTracksPending

return (
<InViewWrapper>
<ExploreSection
Expand All @@ -57,21 +56,22 @@ export const FeaturedRemixContests = () => {
>
{isContestsPageEnabled ? (
<CardList
data={validTrackIds?.map((trackId) => ({ trackId }))}
data={(allContestTrackIds ?? []).map((trackId) => ({ trackId }))}
renderItem={({ item }) => <ContestCard trackId={item.trackId} />}
horizontal
carouselSpacing={spacing.l}
isLoading={isLoading}
isLoading={isAllContestsPending}
LoadingCardComponent={TrackCardSkeleton}
/>
) : (
<CardList
data={validTrackIds?.map((trackId) => ({ trackId }))}
data={remixContests?.map((track) => ({ trackId: track.track_id }))}
renderItem={({ item }) => (
<RemixContestCard trackId={item.trackId} />
)}
horizontal
carouselSpacing={spacing.l}
isLoading={isExplorePending}
LoadingCardComponent={TrackCardSkeleton}
/>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,22 @@ export const ContestsTab = () => {
const isFocused = useIsFocused()
const queryClient = useQueryClient()

// Larger page size + auto-pagination below: the discovery endpoint
// doesn't support `host=…`, so we paginate the global list and
// filter client-side. Bumped from 50 → 100 because hosts whose
// contests sit deep in the global list (ended contests, smaller
// accounts) were missing from the tab — Julian's @dimensionx
// report. Together with the `useEffect` below that drains pages
// until exhausted, this guarantees the host's contests appear once
// they're anywhere in the result set.
const {
data: trackIds,
isPending,
isFetching,
hasNextPage,
fetchNextPage,
isFetchingNextPage
} = useAllRemixContests({ pageSize: 50 }, { enabled: isFocused })
} = useAllRemixContests({ pageSize: 100 }, { enabled: isFocused })

const allTrackIds = useMemo(() => trackIds ?? [], [trackIds])

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
Flex,
IconHeart,
IconKebabHorizontal,
IconStar,
LoadingSpinner,
Paper,
PlainButton,
Expand Down Expand Up @@ -53,7 +54,7 @@ const messages = {
postUpdate: 'Post Update',
attachVideo: '+ Attach Video',
postUpdateBadge: 'Post Update',
artistBadge: 'Artist',
hostBadge: 'Host',
loadMore: 'Load more',
signInToComment: 'Sign in to comment.',
reply: 'Reply',
Expand Down Expand Up @@ -587,42 +588,27 @@ const ContestCommentRow = ({
<Flex direction='row' gap='m' alignItems='flex-start' w='100%'>
<UserAvatar userId={author.user_id} />
<Flex direction='column' gap='xs' css={{ flex: 1, minWidth: 0 }}>
<Flex
gap='s'
alignItems='center'
wrap='wrap'
justifyContent='space-between'
>
<Flex gap='s' alignItems='center' wrap='wrap'>
<UserLink userId={author.user_id} />
{/* Show Artist badge when the host comments in the Comments
feed; suppress the "Post Update" badge in the Updates
feed because every row there is already a host update —
the label was redundant. */}
{isAuthorEventOwner && !isPostUpdate ? (
<Text variant='label' size='xs' color='accent' strength='strong'>
{messages.artistBadge}
{/* Header row mirrors the track-page CommentBlock layout —
user link + timestamp on the left, Host badge on the
right. Overflow kebab moved out of this row and into the
action bar below so it sits next to Reply (matches the
track-page CommentActionBar). */}
<Flex gap='s' alignItems='center' wrap='wrap'>
<UserLink userId={author.user_id} />
{/* Host badge: shown when the contest owner comments in the
Comments feed. Suppressed in the Updates feed because
every row there is already a host update — the label
would be redundant. Visual treatment matches the
track-page CommentBadge (IconStar + accent text). */}
{isAuthorEventOwner && !isPostUpdate ? (
<Flex gap='xs' alignItems='center'>
<IconStar color='accent' size='2xs' />
<Text color='accent' variant='body' size='s'>
{messages.hostBadge}
</Text>
) : null}
{createdAt ? <Timestamp time={createdAt} /> : null}
</Flex>
{canDelete ? (
<PopupMenu
items={[{ text: messages.delete, onClick: handleDelete }]}
renderTrigger={(anchorRef, triggerPopup) => (
<Box
ref={anchorRef as any}
onClick={(e) => {
e.stopPropagation()
triggerPopup()
}}
css={{ cursor: 'pointer', lineHeight: 0 }}
>
<IconKebabHorizontal size='s' color='subdued' />
</Box>
)}
/>
</Flex>
) : null}
{createdAt ? <Timestamp time={createdAt} /> : null}
</Flex>
<Text variant='body' size='s'>
{comment.message}
Expand All @@ -639,6 +625,11 @@ const ContestCommentRow = ({
<VideoEmbed url={videoUrl} />
</Box>
) : null}
{/* Action bar: heart, Reply, overflow kebab. Same shape as the
track-page CommentActionBar so contest comments read like
normal comments. The overflow kebab used to sit in the
header; moving it here stops the row from wrapping when the
user link + timestamp + badge are wide. */}
<Flex gap='m' alignItems='center' pt='xs'>
<Flex
gap='xs'
Expand Down Expand Up @@ -681,6 +672,23 @@ const ContestCommentRow = ({
{messages.reply}
</PlainButton>
) : null}
{canDelete ? (
<PopupMenu
items={[{ text: messages.delete, onClick: handleDelete }]}
renderTrigger={(anchorRef, triggerPopup) => (
<Box
ref={anchorRef as any}
onClick={(e) => {
e.stopPropagation()
triggerPopup()
}}
css={{ cursor: 'pointer', lineHeight: 0 }}
>
<IconKebabHorizontal size='s' color='subdued' />
</Box>
)}
/>
) : null}
</Flex>
{isReplying && currentUserId ? (
<Flex direction='row' gap='s' alignItems='center' pt='xs' w='100%'>
Expand Down
38 changes: 23 additions & 15 deletions packages/web/src/pages/contest-page/components/ContestStemsCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,22 @@ export const ContestStemsCard = ({ trackId }: ContestStemsCardProps) => {
useDownloadTrackArchiveModal()
const { onOpen: openWaitForDownloadModal } = useWaitForDownloadModal()

// Default to expanded so the stems list is visible without an extra
// click. Once we support multiple source tracks per contest we'll flip
// back to collapsed-by-default to keep the surface compact.
const [expanded, setExpanded] = useState(true)
const stemsCount = stems.length
// Default to expanded for short lists so users can see the stems
// without an extra click; collapse when there are more than
// STEMS_COLLAPSE_THRESHOLD entries so a long list doesn't push the
// followers + comments tiles way down the page. The user's explicit
// toggle (override) wins over the heuristic.
const STEMS_COLLAPSE_THRESHOLD = 5
const stemsBelowThreshold = stemsCount <= STEMS_COLLAPSE_THRESHOLD
const [expandedOverride, setExpandedOverride] = useState<boolean | null>(null)
const expanded = expandedOverride ?? stemsBelowThreshold
const setExpanded = (next: boolean | ((prev: boolean) => boolean)) => {
setExpandedOverride((prev) => {
const current = prev ?? stemsBelowThreshold
return typeof next === 'function' ? next(current) : next
})
}

// Same file-size query the track page runs. `stemTracks.map(...)` is
// what feeds per-stem row sizes; adding the parent trackId means
Expand Down Expand Up @@ -223,7 +234,12 @@ export const ContestStemsCard = ({ trackId }: ContestStemsCardProps) => {
role='button'
tabIndex={0}
onClick={handleRowClick}
css={{ cursor: 'pointer' }}
css={{
cursor: 'pointer',
'&:hover .contest-stems-card-title': {
color: color.primary.primary
}
}}
>
<Box
w={64}
Expand All @@ -238,21 +254,13 @@ export const ContestStemsCard = ({ trackId }: ContestStemsCardProps) => {
}}
/>
<Flex direction='column' gap='2xs' css={{ flex: 1, minWidth: 0 }}>
{/* Title underlines + turns accent on its own hover, the way
the UserLink below it does. Previously the whole row
hover scope highlighted the title — that scope was too
broad (just hovering the cover art lit up the title). */}
<Text
variant='title'
size='m'
color='default'
className='contest-stems-card-title'
css={{
cursor: 'pointer',
transition: 'color var(--harmony-quick)',
'&:hover': {
color: color.primary.primary,
textDecoration: 'underline'
}
transition: 'color var(--harmony-quick)'
}}
>
{track.title}
Expand Down
Loading
Loading