From 307ec490c8e359f1b90485dbc96f99bab15f15d7 Mon Sep 17 00:00:00 2001 From: Dylan Jeffers Date: Thu, 7 May 2026 14:33:47 -0700 Subject: [PATCH] fix: pre-fill full remix parent when entering contest from mobile The mobile contest screen's "Enter Contest" button only passed parent_track_id into the upload flow, dropping the user object, has_remix_author_reposted/saved flags, artwork and genre that the existing remix flow already pre-fills. The upload form's remix-of context rendered incomplete on mobile contest entries as a result. Extracts the metadata shape into a shared `useEnterContest` hook in @audius/common/hooks and adds thin web + mobile wrappers that handle their platform-specific artwork blob fetching and navigation. Also dedupes the inline copy in the desktop RemixContestSection so all four entry points (web track page, web contest page, mobile track screen, mobile contest screen) feed the same shape into the upload form. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/common/src/hooks/index.ts | 1 + packages/common/src/hooks/useEnterContest.ts | 50 ++++++++++++++++ packages/mobile/src/hooks/useEnterContest.ts | 43 +++++++++++++ .../screens/contest-screen/ContestScreen.tsx | 13 +--- .../track-screen/UploadRemixFooter.tsx | 60 ++----------------- .../contest-page/hooks/useEnterContest.ts | 55 ++++++----------- .../desktop/RemixContestSection.tsx | 55 ++--------------- 7 files changed, 121 insertions(+), 156 deletions(-) create mode 100644 packages/common/src/hooks/useEnterContest.ts create mode 100644 packages/mobile/src/hooks/useEnterContest.ts diff --git a/packages/common/src/hooks/index.ts b/packages/common/src/hooks/index.ts index cf371a52966..dff7655d696 100644 --- a/packages/common/src/hooks/index.ts +++ b/packages/common/src/hooks/index.ts @@ -43,6 +43,7 @@ export * from './purchaseContent' export * from './content' export * from './chats' export * from './useRemixCountdown' +export * from './useEnterContest' export * from './useFormattedUSDCBalance' export * from './useFormattedAudioBalance' export * from './useFormattedCoinBalance' diff --git a/packages/common/src/hooks/useEnterContest.ts b/packages/common/src/hooks/useEnterContest.ts new file mode 100644 index 00000000000..d6d697ee438 --- /dev/null +++ b/packages/common/src/hooks/useEnterContest.ts @@ -0,0 +1,50 @@ +import { useCallback } from 'react' + +import { useTrack, useUser } from '~/api' +import type { ID } from '~/models' +import type { TrackMetadataForUpload } from '~/store' + +type EnterContestArtwork = { + url: string + file: any +} + +/** + * Source of truth for the "enter remix contest" upload payload — fetches the + * source track + author and returns a builder that produces the + * `initialMetadata` shape both web and mobile feed into the upload flow. + * + * Each platform fetches its own artwork file (Web `File` vs RN's blob shape + * diverge) and passes it back to `buildInitialMetadata`. Centralising the + * `remix_of` shape — including `user` and the boolean flags — keeps the + * contest entry payload from drifting from the existing remix flow. + */ +export const useEnterContest = (trackId: ID | undefined) => { + const { data: originalTrack } = useTrack(trackId) + const { data: originalUser } = useUser(originalTrack?.owner_id) + + const buildInitialMetadata = useCallback( + ( + artwork?: EnterContestArtwork + ): Partial | undefined => { + if (!trackId) return undefined + return { + ...(artwork ? { artwork } : {}), + genre: originalTrack?.genre ?? '', + remix_of: { + tracks: [ + { + parent_track_id: trackId, + user: originalUser, + has_remix_author_reposted: false, + has_remix_author_saved: false + } + ] + } + } + }, + [trackId, originalUser, originalTrack?.genre] + ) + + return { originalTrack, originalUser, buildInitialMetadata } +} diff --git a/packages/mobile/src/hooks/useEnterContest.ts b/packages/mobile/src/hooks/useEnterContest.ts new file mode 100644 index 00000000000..fb1df1cba70 --- /dev/null +++ b/packages/mobile/src/hooks/useEnterContest.ts @@ -0,0 +1,43 @@ +import { useCallback } from 'react' + +import { useEnterContest as useEnterContestShared } from '@audius/common/hooks' +import { type ID, SquareSizes } from '@audius/common/models' + +import { useNavigation } from 'app/hooks/useNavigation' + +/** + * Returns a "submit a remix to this contest" callback that pushes onto the + * Upload navigator with artwork + genre + remix_of pre-filled. Mobile wrapper + * around the shared common hook — handles the React Native blob/file shape + * (RN's `File` object stores its data on `_data`) and the navigation push, + * while the metadata shape itself comes from `@audius/common/hooks`. + */ +export const useEnterContest = (trackId: ID | undefined) => { + const navigation = useNavigation() + const { originalTrack, buildInitialMetadata } = useEnterContestShared(trackId) + + return useCallback(async () => { + if (!trackId) return + + let artwork: { url: string; file: any } | undefined + const imageUrl = originalTrack?.artwork?.[SquareSizes.SIZE_480_BY_480] ?? '' + if (imageUrl) { + const response = await fetch(imageUrl) + const blob = await response.blob() + const file = new File([blob], 'image.jpg', { type: blob.type }) + artwork = { + url: imageUrl, + file: { + // @ts-ignore: KJ - _data is on the file for some reason + ...file._data, + uri: imageUrl + } + } + } + + const initialMetadata = buildInitialMetadata(artwork) + if (!initialMetadata) return + + navigation.push('Upload', { initialMetadata }) + }, [trackId, originalTrack, buildInitialMetadata, navigation]) +} diff --git a/packages/mobile/src/screens/contest-screen/ContestScreen.tsx b/packages/mobile/src/screens/contest-screen/ContestScreen.tsx index 7b671a81c1d..03cf4032cf5 100644 --- a/packages/mobile/src/screens/contest-screen/ContestScreen.tsx +++ b/packages/mobile/src/screens/contest-screen/ContestScreen.tsx @@ -41,6 +41,7 @@ import { collapsibleTabScreen } from 'app/components/top-tab-bar' import { UserLink } from 'app/components/user-link' +import { useEnterContest } from 'app/hooks/useEnterContest' import { useRoute } from 'app/hooks/useRoute' import { setVisibility } from 'app/store/drawers/slice' @@ -294,17 +295,7 @@ export const ContestScreen = () => { dispatch(setVisibility({ drawer: 'PickWinners', visible: true })) }, [trackId, dispatch]) - const handleEnterContest = useCallback(() => { - if (!trackId) - return // Same wire-up as web: jump into the upload flow with `remix_of` - // pre-filled so the resulting track is linked to this contest's - // parent track. The Upload modal stack reads `initialMetadata` off - // its initial route params and merges it into the track metadata - // when the user picks a file (see SelectTrackScreen). - ;(navigation as any).navigate('Upload', { - initialMetadata: { remix_of: { tracks: [{ parent_track_id: trackId }] } } - }) - }, [trackId, navigation]) + const handleEnterContest = useEnterContest(trackId) // Hide the stack navigator header — the in-hero back button is the // only back affordance in the Figma (2888-131647). Leaving the diff --git a/packages/mobile/src/screens/track-screen/UploadRemixFooter.tsx b/packages/mobile/src/screens/track-screen/UploadRemixFooter.tsx index e7970566438..b8f43f42919 100644 --- a/packages/mobile/src/screens/track-screen/UploadRemixFooter.tsx +++ b/packages/mobile/src/screens/track-screen/UploadRemixFooter.tsx @@ -1,10 +1,9 @@ -import React, { useCallback } from 'react' +import React from 'react' -import { useUser, useTrack } from '@audius/common/api' -import { SquareSizes, type ID } from '@audius/common/models' +import type { ID } from '@audius/common/models' import { Button, Flex, IconCloudUpload } from '@audius/harmony-native' -import { useNavigation } from 'app/hooks/useNavigation' +import { useEnterContest } from 'app/hooks/useEnterContest' const messages = { contestEnded: 'Contest Ended', @@ -20,58 +19,7 @@ type UploadRemixFooterProps = { * Footer component for uploading remixes in the remix contest section */ export const UploadRemixFooter = ({ trackId }: UploadRemixFooterProps) => { - const navigation = useNavigation() - const { data: originalTrack } = useTrack(trackId) - const { data: originalUser } = useUser(originalTrack?.owner_id) - - const handlePressSubmitRemix = useCallback(async () => { - if (!trackId) return - - let file: File | undefined - const imageUrl = originalTrack?.artwork?.[SquareSizes.SIZE_480_BY_480] ?? '' - - if (imageUrl) { - const response = await fetch(imageUrl) - const blob = await response.blob() - file = new File([blob], 'image.jpg', { type: blob.type }) - } - - const state = { - initialMetadata: { - ...(file - ? { - artwork: { - url: imageUrl, - file: { - // @ts-ignore: KJ - _data is on the file for some reason - ...file._data, - uri: imageUrl - } - } - } - : {}), - genre: originalTrack?.genre ?? '', - remix_of: { - tracks: [ - { - parent_track_id: trackId, - user: originalUser, - has_remix_author_reposted: false, - has_remix_author_saved: false - } - ] - } - } - } - - navigation.push('Upload', state) - }, [ - navigation, - originalTrack?.artwork, - originalTrack?.genre, - originalUser, - trackId - ]) + const handlePressSubmitRemix = useEnterContest(trackId) return ( { const navigate = useNavigateToPage() - const { data: originalTrack } = useTrack(trackId) - const { data: originalUser } = useUser(originalTrack?.owner_id) + const { originalTrack, buildInitialMetadata } = useEnterContestShared(trackId) return useRequiresAccountCallback(async () => { if (!trackId) return - let file: File | undefined + let artwork: { url: string; file: File } | undefined const imageUrl = originalTrack?.artwork?.[SquareSizes.SIZE_1000_BY_1000] ?? '' if (imageUrl) { const response = await fetch(imageUrl) const blob = await response.blob() - file = new File([blob], 'image.jpg', { type: blob.type }) + artwork = { + url: imageUrl, + file: new File([blob], 'image.jpg', { type: blob.type }) + } } + const initialMetadata = buildInitialMetadata(artwork) + if (!initialMetadata) return + const state: { initialMetadata: Partial } = { - initialMetadata: { - ...(file - ? { - artwork: { - url: imageUrl, - file - } - } - : {}), - genre: originalTrack?.genre ?? '', - remix_of: { - tracks: [ - { - parent_track_id: trackId, - user: originalUser, - has_remix_author_reposted: false, - has_remix_author_saved: false - } - ] - } - } + initialMetadata } navigate(UPLOAD_PAGE, state) - }, [trackId, navigate, originalTrack, originalUser]) + }, [trackId, navigate, originalTrack, buildInitialMetadata]) } diff --git a/packages/web/src/pages/track-page/components/desktop/RemixContestSection.tsx b/packages/web/src/pages/track-page/components/desktop/RemixContestSection.tsx index f6045dade66..449560537ec 100644 --- a/packages/web/src/pages/track-page/components/desktop/RemixContestSection.tsx +++ b/packages/web/src/pages/track-page/components/desktop/RemixContestSection.tsx @@ -1,14 +1,7 @@ import { useState, useCallback } from 'react' -import { - useRemixContest, - useRemixesLineup, - useTrack, - useUser -} from '@audius/common/api' -import { ID, Name, SquareSizes } from '@audius/common/models' -import { UPLOAD_PAGE } from '@audius/common/src/utils/route' -import { TrackMetadataForUpload } from '@audius/common/store' +import { useRemixContest, useRemixesLineup, useTrack } from '@audius/common/api' +import { ID, Name } from '@audius/common/models' import { dayjs } from '@audius/common/utils' import { Box, @@ -23,8 +16,7 @@ import { import { Link, useSearchParams } from 'react-router' import { Tab, TabList } from 'components/tabs' -import { useNavigateToPage } from 'hooks/useNavigateToPage' -import { useRequiresAccountCallback } from 'hooks/useRequiresAccount' +import { useEnterContest } from 'pages/contest-page/hooks/useEnterContest' import { useUpdateSearchParams } from 'pages/search-page/hooks' import { track, make } from 'services/analytics' import { pickWinnersPage } from 'utils/route' @@ -64,9 +56,8 @@ export const RemixContestSection = ({ trackId, isOwner }: RemixContestSectionProps) => { - const navigate = useNavigateToPage() + const goToUploadWithRemix = useEnterContest(trackId) const { data: originalTrack } = useTrack(trackId) - const { data: originalUser } = useUser(originalTrack?.owner_id) const { data: remixContest } = useRemixContest(trackId) const { data: remixes, count: remixCount = 0 } = useRemixesLineup({ trackId, @@ -140,44 +131,6 @@ export const RemixContestSection = ({ } }, [remixContest?.eventId, trackId]) - const goToUploadWithRemix = useRequiresAccountCallback(async () => { - if (!trackId) return - - let file: File | undefined - const imageUrl = - originalTrack?.artwork?.[SquareSizes.SIZE_1000_BY_1000] ?? '' - - if (imageUrl) { - const response = await fetch(imageUrl) - const blob = await response.blob() - file = new File([blob], 'image.jpg', { type: blob.type }) - } - - const state: { initialMetadata: Partial } = { - initialMetadata: { - ...(file - ? { - artwork: { - url: imageUrl, - file - } - } - : {}), - genre: originalTrack?.genre ?? '', - remix_of: { - tracks: [ - { - parent_track_id: trackId, - user: originalUser, - has_remix_author_reposted: false, - has_remix_author_saved: false - } - ] - } - } - } - navigate(UPLOAD_PAGE, state) - }, [trackId, navigate, originalTrack, originalUser]) if (!trackId || !remixContest) return null const totalBoxHeight = TAB_BAR_HEIGHT + contentHeight