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
40 changes: 5 additions & 35 deletions packages/common/src/api/tan-query/coins/useUpdateFanClub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'

import { Coin } from '~/adapters/coin'
import { useCurrentUserId } from '~/api'
import { useQueryContext } from '~/api/tan-query/utils'
import { uploadBackdropImage, useQueryContext } from '~/api/tan-query/utils'

import { QUERY_KEYS } from '../queryKeys'

Expand Down Expand Up @@ -41,29 +41,10 @@ export const useUpdateFanClub = () => {
let bannerImageUrl: string | undefined

if (updateCoinRequest.bannerImageFile) {
const uploadResponse = await sdk.services.storage
.uploadFile({
file: updateCoinRequest.bannerImageFile,
metadata: {
template: 'img_backdrop'
}
})
.start()
const cid = getBannerImageUrl(uploadResponse.results)
if (!cid) {
throw new Error('Failed to process banner image upload')
}

// Convert CID to content node URL (same pattern as logo_uri from relay service)
const contentNodeEndpoint = await (
sdk.services.storage as any
).storageNodeSelector?.getSelectedNode()

if (!contentNodeEndpoint) {
throw new Error('No content node available')
}

bannerImageUrl = `${contentNodeEndpoint}/content/${cid}`
bannerImageUrl = await uploadBackdropImage(
sdk,
updateCoinRequest.bannerImageFile
)
} else if (updateCoinRequest.removeBanner) {
bannerImageUrl = ''
}
Expand Down Expand Up @@ -125,14 +106,3 @@ export const useUpdateFanClub = () => {
}
})
}

const getBannerImageUrl = (results: Record<string, string> = {}) => {
const prioritizedSizes = ['2000x', '1500x', '1280x', '1000x', '640x']
for (const size of prioritizedSizes) {
if (results[size]) {
return results[size]
}
}
const firstResult = Object.values(results)[0]
return firstResult
}
14 changes: 10 additions & 4 deletions packages/common/src/api/tan-query/comments/useMuteUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,14 +99,20 @@ export const useMuteUser = () => {
// Generic toast error
dispatch(toast({ content: messages.muteUserError }))

if (trackId === undefined) return
// No way to know what comment count should be here, so we just reset the query data
queryClient.resetQueries({
queryKey: ['trackCommentCount', trackId]
queryKey: getTrackCommentCountQueryKey(trackId)
})
// Reload data
queryClient.resetQueries({
queryKey: ['trackCommentList', trackId, currentSort]
})
if (currentSort !== undefined) {
queryClient.resetQueries({
queryKey: getTrackCommentListQueryKey({
trackId,
sortMethod: currentSort
})
})
}
}
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,20 @@ export const useMarkNotificationsAsViewed = () => {
})
},
onMutate: () => {
const queryKey = getNotificationUnreadCountQueryKey(currentUserId)
const previousUnreadCount = queryClient.getQueryData(queryKey)
// Optimistically set unread count to 0
queryClient.setQueryData(
getNotificationUnreadCountQueryKey(currentUserId),
0
)
queryClient.setQueryData(queryKey, 0)
return { previousUnreadCount }
},
onError: (_error, _variables, context) => {
// Roll back the optimistic update so the unread badge isn't lost
if (context?.previousUnreadCount !== undefined) {
queryClient.setQueryData(
getNotificationUnreadCountQueryKey(currentUserId),
context.previousUnreadCount
)
}
}
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import type { AudiusSdkWithServices } from '@audius/sdk'
import { describe, it, expect, vi } from 'vitest'

import { uploadBackdropImage } from '../uploadBackdropImage'

const makeSdk = ({
results,
node = 'https://validator.example.com'
}: {
results: Record<string, string>
node?: string | null
}) => {
const uploadFile = vi.fn(() => ({
start: vi.fn(async () => ({ results }))
}))
const sdk = {
services: {
storage: { uploadFile },
storageNodeSelector: { getSelectedNode: vi.fn(async () => node) }
}
} as unknown as AudiusSdkWithServices
return { sdk, uploadFile }
}

const file = new File(['x'], 'banner.png', { type: 'image/png' })

describe('uploadBackdropImage', () => {
it('uploads with the img_backdrop template and returns the largest variant URL', async () => {
const { sdk, uploadFile } = makeSdk({
results: { '640x': 'cid640', '2000x': 'cid2000', '1000x': 'cid1000' }
})

const url = await uploadBackdropImage(sdk, file)

expect(uploadFile).toHaveBeenCalledWith({
file,
metadata: { template: 'img_backdrop' }
})
expect(url).toBe('https://validator.example.com/content/cid2000')
})

it('falls back to any variant when no prioritized size is present', async () => {
const { sdk } = makeSdk({ results: { original: 'cidOriginal' } })
await expect(uploadBackdropImage(sdk, file)).resolves.toBe(
'https://validator.example.com/content/cidOriginal'
)
})

it('throws when the upload returns no CID', async () => {
const { sdk } = makeSdk({ results: {} })
await expect(uploadBackdropImage(sdk, file)).rejects.toThrow()
})

it('throws when no validator node is available', async () => {
const { sdk } = makeSdk({ results: { '2000x': 'cid' }, node: null })
await expect(uploadBackdropImage(sdk, file)).rejects.toThrow(
'No validator node available'
)
})
})
1 change: 1 addition & 0 deletions packages/common/src/api/tan-query/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ export * from './primeTrackData'
export * from './primeUserData'
export * from './QueryContext'
export * from './updateTrackData'
export * from './uploadBackdropImage'
export * from './updateCollectionData'
export * from './useQueries'
48 changes: 48 additions & 0 deletions packages/common/src/api/tan-query/utils/uploadBackdropImage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { AudiusSdkWithServices } from '@audius/sdk'

const PRIORITIZED_BACKDROP_SIZES = ['2000x', '1500x', '1280x', '1000x', '640x']

/**
* Picks the CID of the largest available resized variant from an
* `img_backdrop` upload result, falling back to any variant.
*/
const getBackdropImageCid = (results: Record<string, string> = {}) => {
for (const size of PRIORITIZED_BACKDROP_SIZES) {
if (results[size]) {
return results[size]
}
}
return Object.values(results)[0]
}

/**
* Uploads a wide-aspect image (e.g. a fan club banner or contest cover) using
* the `img_backdrop` template and resolves to a URL on the selected validator
* node that serves the largest resized variant.
*
* Throws if the upload produces no CID or no validator node is available.
*/
export const uploadBackdropImage = async (
sdk: AudiusSdkWithServices,
file: File
): Promise<string> => {
const uploadResponse = await sdk.services.storage
.uploadFile({
file,
metadata: { template: 'img_backdrop' }
})
.start()

const cid = getBackdropImageCid(uploadResponse.results)
if (!cid) {
throw new Error('Failed to process backdrop image upload')
}

const validatorNodeEndpoint =
await sdk.services.storageNodeSelector.getSelectedNode()
if (!validatorNodeEndpoint) {
throw new Error('No validator node available')
}

return `${validatorNodeEndpoint}/content/${cid}`
}
23 changes: 14 additions & 9 deletions packages/mobile/src/hooks/useEnterContest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,21 @@ export const useEnterContest = (trackId: ID | undefined) => {
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
try {
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
}
}
} catch (e) {
// Artwork is only a pre-fill, so continue to upload without it
console.warn('Failed to pre-fill contest artwork', e)
}
}

Expand Down
39 changes: 5 additions & 34 deletions packages/web/src/hooks/useLaunchCoin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
getConnectedWalletsQueryOptions,
getCurrentAccountQueryKey,
getUserQueryKey,
uploadBackdropImage,
useQueryContext
} from '@audius/common/api'
import {
Expand Down Expand Up @@ -184,29 +185,10 @@ export const useLaunchCoin = () => {

let resolvedBannerImageUrl = bannerImageUrl
if (bannerImageFile) {
const uploadResponse = await sdk.services.storage
.uploadFile({
file: bannerImageFile,
metadata: {
template: 'img_backdrop'
}
})
.start()
const cid = getBannerImageUrl(uploadResponse.results)
if (!cid) {
throw new Error('Failed to process banner image upload')
}

// Convert CID to content node URL
const contentNodeEndpoint = await (
sdk.services.storage as any
).storageNodeSelector?.getSelectedNode()

if (!contentNodeEndpoint) {
throw new Error('No content node available')
}

resolvedBannerImageUrl = `${contentNodeEndpoint}/content/${cid}`
resolvedBannerImageUrl = await uploadBackdropImage(
sdk,
bannerImageFile
)
}

// Create coin in Audius database
Expand Down Expand Up @@ -282,14 +264,3 @@ export const useLaunchCoin = () => {
}
})
}

const getBannerImageUrl = (results: Record<string, string> = {}) => {
const prioritizedSizes = ['2000x', '1500x', '1280x', '1000x', '640x']
for (const size of prioritizedSizes) {
if (results[size]) {
return results[size]
}
}
const firstResult = Object.values(results)[0]
return firstResult
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { useCallback, useState } from 'react'

import { useQueryContext } from '@audius/common/api'
import { uploadBackdropImage, useQueryContext } from '@audius/common/api'

/**
* Handles mediorum upload of a contest cover photo. Mirrors the pattern
* used by useLaunchCoin / useUpdateFanClub for banner images — uploads the
* file with `img_backdrop` template (wide aspect-ratio) and resolves to a
* content-node URL the form can persist in `eventData.coverPhotoUrl`.
* Handles upload of a contest cover photo. Uses the shared
* `uploadBackdropImage` helper (also used for fan club banners), which uploads
* the file with the `img_backdrop` template (wide aspect-ratio) and resolves
* to a validator node URL the form can persist in `eventData.coverPhotoUrl`.
*/
export const useUploadContestCover = () => {
const { audiusSdk } = useQueryContext()
Expand All @@ -17,33 +17,7 @@ export const useUploadContestCover = () => {
setIsUploading(true)
try {
const sdk = await audiusSdk()
const uploadResponse = await sdk.services.storage
.uploadFile({
file,
metadata: { template: 'img_backdrop' }
})
.start()

const results = (uploadResponse.results ?? {}) as Record<string, string>
const prioritizedSizes = ['2000x', '1500x', '1280x', '1000x', '640x']
let cid: string | undefined
for (const size of prioritizedSizes) {
if (results[size]) {
cid = results[size]
break
}
}
if (!cid) cid = Object.values(results)[0]
if (!cid) throw new Error('Upload returned no CID')

const contentNodeEndpoint = await (
sdk.services.storage as any
).storageNodeSelector?.getSelectedNode()
if (!contentNodeEndpoint) {
throw new Error('No content node available')
}

return `${contentNodeEndpoint}/content/${cid}`
return await uploadBackdropImage(sdk, file)
} catch (error) {
console.error('HostRemixContest', error as Error)
return null
Expand Down
Loading
Loading