1- import React , { useMemo , useState } from 'react' ;
1+ import React , { useEffect , useMemo , useRef , useState } from 'react' ;
22import { StyleSheet , View } from 'react-native' ;
33import { Image } from 'expo-image' ;
44import SkeletonPlaceholder from 'react-native-skeleton-placeholder' ;
@@ -30,6 +30,20 @@ const styles = StyleSheet.create({
3030 borderRadius : BORDER_RADIUS ,
3131 justifyContent : 'center' ,
3232 alignItems : 'center'
33+ } ,
34+ imageContainer : {
35+ width : SIZE ,
36+ height : SIZE ,
37+ position : 'relative'
38+ } ,
39+ skeletonOverlay : {
40+ position : 'absolute' ,
41+ top : 0 ,
42+ left : 0 ,
43+ right : 0 ,
44+ bottom : 0 ,
45+ justifyContent : 'center' ,
46+ alignItems : 'center'
3347 }
3448} ) ;
3549
@@ -53,38 +67,45 @@ const ServerAvatar = React.memo(({ url, image }: IServerAvatar) => {
5367 return `${ baseUri } ${ separator } _cb=${ encodeURIComponent ( image ) } ` ;
5468 } , [ url , image ] ) ;
5569
56- // Initialize loading state - will be reset by onLoadStart when Image mounts
70+ // Track loading/error for the current imageUri and reset when it changes
71+ const previousImageUriRef = useRef < string | null > ( imageUri ) ;
5772 const [ loading , setLoading ] = useState ( ( ) => ! ! imageUri ) ;
5873 const [ error , setError ] = useState ( false ) ;
5974
60- const handleLoadStart = ( ) => {
61- setLoading ( true ) ;
62- setError ( false ) ;
63- } ;
75+ // Reset loading/error state when imageUri changes
76+ // This ensures a new image URL can recover from previous errors
77+ // Note: Using useEffect here is necessary to reset state when props change.
78+ // The linter warning about cascading renders is acceptable here as we need
79+ // to synchronize component state with prop changes for proper error recovery.
80+ useEffect ( ( ) => {
81+ // Only update if imageUri actually changed
82+ if ( previousImageUriRef . current !== imageUri ) {
83+ previousImageUriRef . current = imageUri ;
84+ if ( imageUri ) {
85+ // New image: start in loading state and clear previous errors
86+ setLoading ( true ) ;
87+ setError ( false ) ;
88+ } else {
89+ // No image: nothing to load, no error
90+ setLoading ( false ) ;
91+ setError ( false ) ;
92+ }
93+ }
94+ } , [ imageUri ] ) ;
6495
6596 const handleLoad = ( ) => {
6697 setLoading ( false ) ;
67- setError ( false ) ;
6898 } ;
6999
70100 const handleError = ( ) => {
71101 setLoading ( false ) ;
72102 setError ( true ) ;
73103 } ;
74104
75- // Show skeleton while loading
76- if ( loading && imageUri && ! error ) {
77- return (
78- < View style = { styles . container } >
79- < SkeletonPlaceholder borderRadius = { BORDER_RADIUS } backgroundColor = { colors . surfaceNeutral } >
80- < SkeletonPlaceholder . Item width = { SIZE } height = { SIZE } borderRadius = { BORDER_RADIUS } />
81- </ SkeletonPlaceholder >
82- </ View >
83- ) ;
84- }
105+ const showFallback = error || ! imageUri ;
85106
86107 // Show fallback icon on error or when no image is provided
87- if ( error || ! imageUri ) {
108+ if ( showFallback ) {
88109 return (
89110 < View style = { styles . container } >
90111 < View
@@ -102,25 +123,33 @@ const ServerAvatar = React.memo(({ url, image }: IServerAvatar) => {
102123 ) ;
103124 }
104125
105- // Show the actual image
126+ // Show the actual image with an overlaid skeleton while loading
106127 return (
107128 < View style = { styles . container } >
108- < Image
109- key = { `${ imageUri } -${ image } ` }
110- style = { [
111- styles . image ,
112- {
113- borderColor : colors . strokeLight ,
114- borderWidth : 1 ,
115- backgroundColor : isDarkMode ? colors . surfaceNeutral : 'transparent'
116- }
117- ] }
118- source = { { uri : imageUri } }
119- onLoadStart = { handleLoadStart }
120- onLoad = { handleLoad }
121- onError = { handleError }
122- contentFit = 'cover'
123- />
129+ < View style = { styles . imageContainer } >
130+ < Image
131+ key = { `${ imageUri } -${ image } ` }
132+ style = { [
133+ styles . image ,
134+ {
135+ borderColor : colors . strokeLight ,
136+ borderWidth : 1 ,
137+ backgroundColor : isDarkMode ? colors . surfaceNeutral : 'transparent'
138+ }
139+ ] }
140+ source = { { uri : imageUri } }
141+ onLoad = { handleLoad }
142+ onError = { handleError }
143+ contentFit = 'cover'
144+ />
145+ { loading && (
146+ < View style = { styles . skeletonOverlay } >
147+ < SkeletonPlaceholder borderRadius = { BORDER_RADIUS } backgroundColor = { colors . surfaceNeutral } >
148+ < SkeletonPlaceholder . Item width = { SIZE } height = { SIZE } borderRadius = { BORDER_RADIUS } />
149+ </ SkeletonPlaceholder >
150+ </ View >
151+ ) }
152+ </ View >
124153 </ View >
125154 ) ;
126155} ) ;
0 commit comments