Skip to content

Commit 1b64c9f

Browse files
fix: ServerAvatar loading/error state control flow
- Fix skeleton branch preventing Image from loading by always rendering Image component - Overlay skeleton on top of Image instead of early-returning - Reset loading/error state when imageUri changes using useEffect - Remove onLoadStart handler since loading state is managed via useEffect - Ensure error state resets when image URL/ETag changes for recovery from transient failures This fixes the issue where skeleton would permanently hide the image and error state would block recovery after errors.
1 parent b7d481d commit 1b64c9f

File tree

1 file changed

+64
-35
lines changed

1 file changed

+64
-35
lines changed

app/views/WorkspaceView/ServerAvatar.tsx

Lines changed: 64 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useMemo, useState } from 'react';
1+
import React, { useEffect, useMemo, useRef, useState } from 'react';
22
import { StyleSheet, View } from 'react-native';
33
import { Image } from 'expo-image';
44
import 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

Comments
 (0)