Skip to content

Commit bd3f3cf

Browse files
Fix mobile image fallback logic (#12549)
1 parent 1a752dd commit bd3f3cf

File tree

12 files changed

+146
-49
lines changed

12 files changed

+146
-49
lines changed

packages/common/src/hooks/useImageSize.ts

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,14 @@ export const useImageSize = <
5757
preloadImageFn?: (url: string) => Promise<void>
5858
}) => {
5959
const [imageUrl, setImageUrl] = useState<Maybe<string>>(undefined)
60+
const [failedUrls, setFailedUrls] = useState<Set<string>>(new Set())
6061

6162
const fetchWithFallback = useCallback(
6263
async (url: string) => {
6364
const mirrors = [...(artwork?.mirrors ?? [])]
6465
let currentUrl = url
6566

66-
while (mirrors.length > 0) {
67+
while (mirrors.length >= 0) {
6768
try {
6869
await preloadImageFn?.(currentUrl)
6970
return currentUrl
@@ -81,6 +82,25 @@ export const useImageSize = <
8182
[artwork?.mirrors, preloadImageFn]
8283
)
8384

85+
const getNextMirrorUrl = useCallback(
86+
(originalUrl: string, failedUrls: Set<string>) => {
87+
const mirrors = artwork?.mirrors ?? []
88+
if (mirrors.length === 0) return null
89+
90+
for (const mirror of mirrors) {
91+
const nextUrl = new URL(originalUrl)
92+
nextUrl.hostname = new URL(mirror).hostname
93+
const mirrorUrl = nextUrl.toString()
94+
95+
if (!failedUrls.has(mirrorUrl)) {
96+
return mirrorUrl
97+
}
98+
}
99+
return null
100+
},
101+
[artwork?.mirrors]
102+
)
103+
84104
const resolveImageUrl = useCallback(async () => {
85105
if (!artwork) {
86106
return
@@ -91,6 +111,15 @@ export const useImageSize = <
91111
return
92112
}
93113

114+
// If the original URL failed, try mirrors
115+
if (failedUrls.has(targetUrl)) {
116+
const mirrorUrl = getNextMirrorUrl(targetUrl, failedUrls)
117+
if (mirrorUrl) {
118+
setImageUrl(mirrorUrl)
119+
return
120+
}
121+
}
122+
94123
if (IMAGE_CACHE.has(targetUrl)) {
95124
setImageUrl(targetUrl)
96125
return
@@ -136,11 +165,25 @@ export const useImageSize = <
136165
} catch (e) {
137166
console.error(`Unable to load image ${targetUrl} after retries: ${e}`)
138167
}
139-
}, [artwork, targetSize, fetchWithFallback, defaultImage])
168+
}, [
169+
artwork,
170+
targetSize,
171+
fetchWithFallback,
172+
defaultImage,
173+
failedUrls,
174+
getNextMirrorUrl
175+
])
176+
177+
const onError = useCallback(
178+
(url: string) => {
179+
setFailedUrls((prev) => new Set(prev).add(url))
180+
},
181+
[setFailedUrls]
182+
)
140183

141184
useEffect(() => {
142185
resolveImageUrl()
143186
}, [resolveImageUrl])
144187

145-
return imageUrl
188+
return { imageUrl, onError }
146189
}

packages/mobile/src/components/audio/GoogleCast.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export const useChromecast = () => {
4343
const previousCastState = usePrevious(castState)
4444

4545
const [internalCounter, setInternalCounter] = useState(0)
46-
const imageUrl = useImageSize({
46+
const { imageUrl } = useImageSize({
4747
artwork: track?.artwork,
4848
targetSize: SquareSizes.SIZE_1000_BY_1000
4949
})

packages/mobile/src/components/image/CollectionImage.tsx

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { useCallback } from 'react'
2+
13
import { useCollection } from '@audius/common/api'
24
import { useImageSize } from '@audius/common/hooks'
35
import type { SquareSizes, ID } from '@audius/common/models'
@@ -49,7 +51,7 @@ export const useCollectionImage = ({
4951
const { data: artwork } = useCollection(collectionId, {
5052
select: (collection) => collection.artwork
5153
})
52-
const image = useImageSize({
54+
const { imageUrl, onError } = useImageSize({
5355
artwork,
5456
targetSize: size,
5557
defaultImage: '',
@@ -58,10 +60,11 @@ export const useCollectionImage = ({
5860
}
5961
})
6062

61-
if (image === '') {
63+
if (imageUrl === '') {
6264
return {
6365
source: imageEmpty,
64-
isFallbackImage: true
66+
isFallbackImage: true,
67+
onError
6568
}
6669
}
6770

@@ -73,13 +76,15 @@ export const useCollectionImage = ({
7376
return {
7477
// @ts-ignore
7578
source: primitiveToImageSource(artwork.url),
76-
isFallbackImage: false
79+
isFallbackImage: false,
80+
onError
7781
}
7882
}
7983

8084
return {
81-
source: primitiveToImageSource(image),
82-
isFallbackImage: false
85+
source: primitiveToImageSource(imageUrl),
86+
isFallbackImage: false,
87+
onError
8388
}
8489
}
8590

@@ -98,10 +103,20 @@ export const CollectionImage = (props: CollectionImageProps) => {
98103
const collectionImageSource = useCollectionImage({ collectionId, size })
99104
const { cornerRadius } = useTheme()
100105
const { skeleton } = useThemeColors()
101-
const { source: loadedSource, isFallbackImage } = collectionImageSource
106+
const {
107+
source: loadedSource,
108+
isFallbackImage,
109+
onError
110+
} = collectionImageSource
102111

103112
const source = loadedSource ?? localCollectionImageUri
104113

114+
const handleError = useCallback(() => {
115+
if (source && typeof source === 'object' && 'uri' in source && source.uri) {
116+
onError(source.uri)
117+
}
118+
}, [source, onError])
119+
105120
return (
106121
<FastImage
107122
{...other}
@@ -112,8 +127,9 @@ export const CollectionImage = (props: CollectionImageProps) => {
112127
},
113128
style
114129
]}
115-
source={source ?? { uri: '' }}
130+
source={source}
116131
onLoad={onLoad}
132+
onError={handleError}
117133
/>
118134
)
119135
}

packages/mobile/src/components/image/CoverPhoto.tsx

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { useCallback } from 'react'
2+
13
import { useUser } from '@audius/common/api'
24
import { useImageSize } from '@audius/common/hooks'
35
import type { ID } from '@audius/common/models'
@@ -40,7 +42,7 @@ export const useCoverPhoto = ({
4042
})
4143
const { cover_photo, updatedCoverPhoto } = partialUser ?? {}
4244
const coverPhoto = cover_photo
43-
const image = useImageSize({
45+
const { imageUrl, onError } = useImageSize({
4446
artwork: coverPhoto,
4547
targetSize: size,
4648
defaultImage: '',
@@ -49,20 +51,21 @@ export const useCoverPhoto = ({
4951
}
5052
})
5153

52-
const isDefaultCover = image === ''
54+
const isDefaultCover = imageUrl === ''
5355
const shouldBlur = isDefaultCover && !isDefaultProfile
5456

5557
if (updatedCoverPhoto && !shouldBlur) {
5658
return {
5759
source: primitiveToImageSource(updatedCoverPhoto.url),
58-
shouldBlur
60+
shouldBlur,
61+
onError
5962
}
6063
}
6164

6265
if (shouldBlur) {
63-
return { source: profilePicture, shouldBlur }
66+
return { source: profilePicture, shouldBlur, onError }
6467
}
65-
return { source: primitiveToImageSource(image), shouldBlur }
68+
return { source: primitiveToImageSource(imageUrl), shouldBlur, onError }
6669
}
6770

6871
type CoverPhotoProps = {
@@ -73,7 +76,7 @@ export const CoverPhoto = (props: CoverPhotoProps) => {
7376
const { userId, ...imageProps } = props
7477
const scrollY = useCurrentTabScrollY()
7578

76-
const { source, shouldBlur } = useCoverPhoto({
79+
const { source, shouldBlur, onError } = useCoverPhoto({
7780
userId,
7881
size: WidthSizes.SIZE_640
7982
})
@@ -104,11 +107,17 @@ export const CoverPhoto = (props: CoverPhotoProps) => {
104107
})
105108
}))
106109

110+
const handleError = useCallback(() => {
111+
if (source && typeof source === 'object' && 'uri' in source && source.uri) {
112+
onError(source.uri)
113+
}
114+
}, [source, onError])
115+
107116
if (!source) return null
108117

109118
return (
110119
<Animated.View style={animatedStyle}>
111-
<FastImage source={source} {...imageProps}>
120+
<FastImage source={source} {...imageProps} onError={handleError}>
112121
{shouldBlur || scrollY ? (
113122
<AnimatedBlurView
114123
blurType='light'

packages/mobile/src/components/image/TrackImage.tsx

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { useCallback } from 'react'
2+
13
import { useTrack } from '@audius/common/api'
24
import { useImageSize } from '@audius/common/hooks'
35
import type { SquareSizes, ID } from '@audius/common/models'
@@ -49,7 +51,7 @@ export const useTrackImage = ({
4951
return track.artwork
5052
}
5153
})
52-
const image = useImageSize({
54+
const { imageUrl, onError } = useImageSize({
5355
artwork,
5456
targetSize: size,
5557
defaultImage: '',
@@ -58,10 +60,11 @@ export const useTrackImage = ({
5860
}
5961
})
6062

61-
if (image === '') {
63+
if (imageUrl === '') {
6264
return {
6365
source: imageEmpty,
64-
isFallbackImage: true
66+
isFallbackImage: true,
67+
onError
6568
}
6669
}
6770

@@ -73,13 +76,15 @@ export const useTrackImage = ({
7376
return {
7477
// @ts-ignore
7578
source: primitiveToImageSource(artwork.url),
76-
isFallbackImage: false
79+
isFallbackImage: false,
80+
onError
7781
}
7882
}
7983

8084
return {
81-
source: primitiveToImageSource(image),
82-
isFallbackImage: false
85+
source: primitiveToImageSource(imageUrl),
86+
isFallbackImage: false,
87+
onError
8388
}
8489
}
8590

@@ -89,6 +94,7 @@ type TrackImageProps = {
8994
style?: FastImageProps['style']
9095
borderRadius?: CornerRadiusOptions
9196
onLoad?: FastImageProps['onLoad']
97+
onError?: FastImageProps['onError']
9298
children?: React.ReactNode
9399
}
94100

@@ -106,10 +112,21 @@ export const TrackImage = (props: TrackImageProps) => {
106112
const trackImageSource = useTrackImage({ trackId, size })
107113
const { cornerRadius } = useTheme()
108114
const { skeleton } = useThemeColors()
109-
const { source: loadedSource, isFallbackImage } = trackImageSource
115+
const { source: loadedSource, isFallbackImage, onError } = trackImageSource
110116

111117
const source = loadedSource ?? localTrackImageUri
112118

119+
const handleError = useCallback(() => {
120+
if (
121+
source &&
122+
typeof source === 'object' &&
123+
'uri' in source &&
124+
typeof source.uri === 'string'
125+
) {
126+
onError(source.uri)
127+
}
128+
}, [source, onError])
129+
113130
return (
114131
<FastImage
115132
{...other}
@@ -120,7 +137,8 @@ export const TrackImage = (props: TrackImageProps) => {
120137
},
121138
style
122139
]}
123-
source={source ?? { uri: '' }}
140+
source={source}
141+
onError={handleError}
124142
onLoad={onLoad}
125143
/>
126144
)

packages/mobile/src/components/image/UserImage.tsx

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { useCallback } from 'react'
2+
13
import { useUser } from '@audius/common/api'
24
import { useImageSize } from '@audius/common/hooks'
35
import type { SquareSizes, ID } from '@audius/common/models'
@@ -28,7 +30,7 @@ export const useProfilePicture = ({
2830
})
2931

3032
const { profile_picture, updatedProfilePicture } = partialUser ?? {}
31-
const image = useImageSize({
33+
const { imageUrl, onError } = useImageSize({
3234
artwork: profile_picture,
3335
targetSize: size,
3436
defaultImage: '',
@@ -37,31 +39,40 @@ export const useProfilePicture = ({
3739
}
3840
})
3941

40-
if (image === '') {
42+
if (imageUrl === '') {
4143
return {
4244
source: profilePicEmpty,
43-
isFallbackImage: true
45+
isFallbackImage: true,
46+
onError
4447
}
4548
}
4649

4750
if (updatedProfilePicture) {
4851
return {
4952
source: primitiveToImageSource(updatedProfilePicture.url),
50-
isFallbackImage: false
53+
isFallbackImage: false,
54+
onError
5155
}
5256
}
5357

5458
return {
55-
source: primitiveToImageSource(image),
56-
isFallbackImage: false
59+
source: primitiveToImageSource(imageUrl),
60+
isFallbackImage: false,
61+
onError
5762
}
5863
}
5964

6065
export type UserImageProps = UseUserImageOptions & Partial<FastImageProps>
6166

6267
export const UserImage = (props: UserImageProps) => {
6368
const { userId, size, ...imageProps } = props
64-
const { source } = useProfilePicture({ userId, size })
69+
const { source, onError } = useProfilePicture({ userId, size })
70+
71+
const handleError = useCallback(() => {
72+
if (source && typeof source === 'object' && 'uri' in source && source.uri) {
73+
onError(source.uri)
74+
}
75+
}, [source, onError])
6576

66-
return <FastImage {...imageProps} source={source ?? { uri: '' }} />
77+
return <FastImage {...imageProps} source={source} onError={handleError} />
6778
}

0 commit comments

Comments
 (0)