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
4 changes: 2 additions & 2 deletions packages/common/src/adapters/collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ export const playlistMetadataForCreateWithSDK = (
parentalWarningType: input.parental_warning_type ?? undefined,
...('cover_art_sizes' in input
? {
coverArtCid: input.cover_art_sizes ?? '',
playlistImageSizesMultihash: input.cover_art_sizes ?? '',
isImageAutogenerated: input.is_image_autogenerated ?? false
}
: {})
Expand All @@ -211,7 +211,7 @@ export const playlistMetadataForUpdateWithSDK = (
: undefined,
playlistName: input.playlist_name ?? '',
description: input.description ?? '',
coverArtCid: input.cover_art_sizes ?? '',
playlistImageSizesMultihash: input.cover_art_sizes ?? '',
isPrivate: input.is_private ?? false
}
}
Expand Down
111 changes: 90 additions & 21 deletions packages/mobile/src/components/image/CollectionImage.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,36 @@
import { useState } from 'react'

import { useCollection } from '@audius/common/api'
import { useImageSize } from '@audius/common/hooks'
import type { SquareSizes, ID } from '@audius/common/models'
import { reachabilitySelectors } from '@audius/common/store'
import type { Maybe } from '@audius/common/utils'
import type { LayoutChangeEvent } from 'react-native'
import { View } from 'react-native'
import { useSelector } from 'react-redux'

import { Artwork, preload } from '@audius/harmony-native'
import { Artwork, IconImage, preload } from '@audius/harmony-native'
import type { ImageProps } from '@audius/harmony-native'
import imageEmpty from 'app/assets/images/imageBlank2x.png'
import { getLocalCollectionCoverArtPath } from 'app/services/offline-downloader'
import { getCollectionDownloadStatus } from 'app/store/offline-downloads/selectors'
import { OfflineDownloadStatus } from 'app/store/offline-downloads/slice'
import { useThemeColors } from 'app/utils/theme'

import { primitiveToImageSource } from './primitiveToImageSource'

const { getIsReachable } = reachabilitySelectors

const EMPTY_ICON_MIN = 12
const EMPTY_ICON_MAX = 128
const EMPTY_ICON_RATIO = 0.35

const hasValidArtwork = (artwork: unknown): boolean =>
!!artwork &&
typeof artwork === 'object' &&
Object.entries(artwork as Record<string, unknown>).some(
([k, v]) => k !== 'mirrors' && typeof v === 'string' && v.length > 0
)

export const useLocalCollectionImageUri = (collectionId: Maybe<ID>) => {
const collectionImageUri = useSelector((state) => {
if (!collectionId) return null
Expand Down Expand Up @@ -45,9 +60,17 @@ export const useCollectionImage = ({
collectionId: Maybe<ID>
size: SquareSizes
}) => {
const { data: artwork } = useCollection(collectionId, {
select: (collection) => collection.artwork
const { data: artworkData } = useCollection(collectionId, {
select: (collection) =>
collection != null
? {
artwork: collection.artwork,
hasNoArtwork: !hasValidArtwork(collection.artwork)
}
: undefined
})
const artwork = artworkData?.artwork
const hasNoArtwork = artworkData?.hasNoArtwork ?? false
const { imageUrl, onError: onImageError } = useImageSize({
artwork,
targetSize: size,
Expand All @@ -57,30 +80,25 @@ export const useCollectionImage = ({
}
})

if (imageUrl === '') {
return {
source: imageEmpty,
isFallbackImage: true,
onError: onImageError
}
if (hasNoArtwork || artworkData === undefined) {
return { source: undefined, hasNoArtwork: true, onError: onImageError }
}

// Return edited artwork from this session, if it exists
// TODO(PAY-3588) Update field once we've switched to another property name
// for local changes to artwork
// @ts-ignore
if (artwork?.url) {
return {
// @ts-ignore
source: primitiveToImageSource(artwork.url),
isFallbackImage: false,
hasNoArtwork: false,
onError: onImageError
}
}

if (imageUrl === '') {
return { source: undefined, hasNoArtwork: true, onError: onImageError }
}

return {
source: primitiveToImageSource(imageUrl),
isFallbackImage: false,
hasNoArtwork: false,
onError: onImageError
}
}
Expand All @@ -97,11 +115,39 @@ type CollectionImageProps = {
export const CollectionImage = (props: CollectionImageProps) => {
const { collectionId, size, style, onLoad, onError, ...other } = props

const { staticWhite } = useThemeColors()
const [containerSize, setContainerSize] = useState({ w: 0, h: 0 })
const localCollectionImageUri = useLocalCollectionImageUri(collectionId)
const collectionImageSource = useCollectionImage({ collectionId, size })
const { source: loadedSource, onError: onImageError } = collectionImageSource

const source = loadedSource ?? localCollectionImageUri
const {
source: loadedSource,
onError: onImageError,
hasNoArtwork
} = collectionImageSource

const onEmptyStateLayout = (e: LayoutChangeEvent) => {
const { width, height } = e.nativeEvent.layout
setContainerSize((prev) =>
prev.w === width && prev.h === height ? prev : { w: width, h: height }
)
}
const emptyIconSize =
containerSize.w > 0 && containerSize.h > 0
? Math.round(
Math.min(
EMPTY_ICON_MAX,
Math.max(
EMPTY_ICON_MIN,
Math.min(containerSize.w, containerSize.h) * EMPTY_ICON_RATIO
)
)
)
: EMPTY_ICON_MIN

const source =
hasNoArtwork === true
? undefined
: (loadedSource ?? localCollectionImageUri)

const handleError = (error: { nativeEvent: { error: string } }) => {
if (source && typeof source === 'object' && 'uri' in source) {
Expand All @@ -117,6 +163,29 @@ export const CollectionImage = (props: CollectionImageProps) => {
onLoad={onLoad}
onError={handleError}
style={style}
/>
>
{hasNoArtwork ? (
<View
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
justifyContent: 'center',
alignItems: 'center',
flex: 1
}}
onLayout={onEmptyStateLayout}
pointerEvents='none'
>
<IconImage
height={emptyIconSize}
width={emptyIconSize}
fill={staticWhite}
/>
</View>
) : null}
</Artwork>
)
}
109 changes: 89 additions & 20 deletions packages/mobile/src/components/image/TrackImage.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,36 @@
import { useState } from 'react'

import { useTrack } from '@audius/common/api'
import { useImageSize } from '@audius/common/hooks'
import type { SquareSizes, ID } from '@audius/common/models'
import { reachabilitySelectors } from '@audius/common/store'
import type { Maybe } from '@audius/common/utils'
import type { LayoutChangeEvent } from 'react-native'
import { View } from 'react-native'
import { useSelector } from 'react-redux'

import type { CornerRadiusOptions, ImageProps } from '@audius/harmony-native'
import { Artwork, preload } from '@audius/harmony-native'
import imageEmpty from 'app/assets/images/imageBlank2x.png'
import { Artwork, IconImage, preload } from '@audius/harmony-native'
import { getLocalTrackCoverArtPath } from 'app/services/offline-downloader'
import { getTrackDownloadStatus } from 'app/store/offline-downloads/selectors'
import { OfflineDownloadStatus } from 'app/store/offline-downloads/slice'
import { useThemeColors } from 'app/utils/theme'

import { primitiveToImageSource } from './primitiveToImageSource'

const { getIsReachable } = reachabilitySelectors

const EMPTY_ICON_MIN = 12
const EMPTY_ICON_MAX = 128
const EMPTY_ICON_RATIO = 0.35

const hasValidArtwork = (artwork: unknown): boolean =>
!!artwork &&
typeof artwork === 'object' &&
Object.entries(artwork as Record<string, unknown>).some(
([k, v]) => k !== 'mirrors' && typeof v === 'string' && v.length > 0
)

const useLocalTrackImageUri = (trackId: Maybe<ID>) => {
const trackImageUri = useSelector((state) => {
if (!trackId) return null
Expand All @@ -40,11 +55,17 @@ export const useTrackImage = ({
trackId?: ID
size: SquareSizes
}) => {
const { data: artwork } = useTrack(trackId, {
select: (track) => {
return track.artwork
}
const { data: artworkData } = useTrack(trackId, {
select: (track) =>
track != null
? {
artwork: track.artwork,
hasNoArtwork: !hasValidArtwork(track.artwork)
}
: undefined
})
const artwork = artworkData?.artwork
const hasNoArtwork = artworkData?.hasNoArtwork ?? false
const { imageUrl, onError: onImageError } = useImageSize({
artwork,
targetSize: size,
Expand All @@ -54,29 +75,29 @@ export const useTrackImage = ({
}
})

if (imageUrl === '') {
return {
source: imageEmpty,
isFallbackImage: true
}
// When track has no artwork or track not loaded yet, don't pass a URL so we never show stale image
if (hasNoArtwork || artworkData === undefined) {
return { source: undefined, hasNoArtwork: true }
}

// Return edited artwork from this session, if it exists
// TODO(PAY-3588) Update field once we've switched to another property name
// for local changes to artwork
// @ts-ignore
// @ts-expect-error - url is added for in-session edits
if (artwork?.url) {
return {
// @ts-ignore
// @ts-expect-error - url is added for in-session edits
source: primitiveToImageSource(artwork.url),
isFallbackImage: false,
hasNoArtwork: false,
onError: onImageError
}
}

if (imageUrl === '') {
return { source: undefined, hasNoArtwork: true }
}

return {
source: primitiveToImageSource(imageUrl),
isFallbackImage: false,
hasNoArtwork: false,
onError: onImageError
}
}
Expand All @@ -102,11 +123,37 @@ export const TrackImage = (props: TrackImageProps) => {
children
} = props

const { staticWhite } = useThemeColors()
const [containerSize, setContainerSize] = useState({ w: 0, h: 0 })
const localTrackImageUri = useLocalTrackImageUri(trackId)
const trackImageSource = useTrackImage({ trackId, size })
const { source: loadedSource, onError: onImageError } = trackImageSource

const source = loadedSource ?? localTrackImageUri
const {
source: loadedSource,
onError: onImageError,
hasNoArtwork
} = trackImageSource

const onEmptyStateLayout = (e: LayoutChangeEvent) => {
const { width, height } = e.nativeEvent.layout
setContainerSize((prev) =>
prev.w === width && prev.h === height ? prev : { w: width, h: height }
)
}
const emptyIconSize =
containerSize.w > 0 && containerSize.h > 0
? Math.round(
Math.min(
EMPTY_ICON_MAX,
Math.max(
EMPTY_ICON_MIN,
Math.min(containerSize.w, containerSize.h) * EMPTY_ICON_RATIO
)
)
)
: EMPTY_ICON_MIN

const source =
hasNoArtwork === true ? undefined : (loadedSource ?? localTrackImageUri)

const handleError = (error: any) => {
try {
Expand Down Expand Up @@ -134,6 +181,28 @@ export const TrackImage = (props: TrackImageProps) => {
borderRadius={borderRadius}
style={style}
>
{hasNoArtwork ? (
<View
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
justifyContent: 'center',
alignItems: 'center',
flex: 1
}}
onLayout={onEmptyStateLayout}
pointerEvents='none'
>
<IconImage
height={emptyIconSize}
width={emptyIconSize}
fill={staticWhite}
/>
</View>
) : null}
{children}
</Artwork>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export const Artwork = ({ track }: ArtworkProps) => {
return (
<View style={[styles.root, { shadowColor }]}>
<TrackImage
key={track?.track_id}
trackId={track?.track_id}
style={styles.image}
size={SquareSizes.SIZE_1000_BY_1000}
Expand Down
4 changes: 3 additions & 1 deletion packages/mobile/src/harmony-native/components/Artwork.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ export const Artwork = (props: ArtworkProps) => {
borderRadius={borderRadius}
border='default'
shadow={shadow}
w='100%'
h='100%'
style={{ borderWidth }}
>
{isLoading && hasImageSource ? (
Expand All @@ -102,7 +104,7 @@ export const Artwork = (props: ArtworkProps) => {
style={{
backgroundColor:
!hasImageSource && children
? color.neutral.n400
? color.neutral.n100
: color.background.surface2
}}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ const CollectionItem = ({
collection,
collectionType
}: CollectionItemProps) => {
const image = useCollectionCoverArt({
const { imageUrl: image } = useCollectionCoverArt({
collectionId: collection.playlist_id,
size: SquareSizes.SIZE_150_BY_150
})
Expand Down
Loading