import React from 'react'; import { Image, ImageProps, StyleSheet, Text, View } from 'react-native'; import { DEFAULT_PLANT_IMAGE_URI, getCategoryFallbackImage, getPlantImageSourceFallbackUri, getWikimediaFilePathFromThumbnailUrl, resolveImageUri, tryResolveImageUri, } from '../utils/imageUri'; type SafeImageFallbackMode = 'category' | 'default' | 'none'; interface SafeImageProps extends Omit { uri?: string | null; categories?: string[]; fallbackMode?: SafeImageFallbackMode; placeholderLabel?: string; } const getPlaceholderInitial = (label?: string): string => { if (!label) return '?'; const trimmed = label.trim(); if (!trimmed) return '?'; return trimmed.charAt(0).toUpperCase(); }; export const SafeImage: React.FC = ({ uri, categories, fallbackMode = 'category', placeholderLabel, onError, style, ...props }) => { const categoryFallback = categories && categories.length > 0 ? getCategoryFallbackImage(categories) : DEFAULT_PLANT_IMAGE_URI; const selectedFallback = fallbackMode === 'category' ? categoryFallback : DEFAULT_PLANT_IMAGE_URI; const [resolvedUri, setResolvedUri] = React.useState(() => { const strictResolved = tryResolveImageUri(uri); if (strictResolved) return strictResolved; return fallbackMode === 'none' ? '' : selectedFallback; }); const [showPlaceholder, setShowPlaceholder] = React.useState(() => { if (fallbackMode !== 'none') return false; return !tryResolveImageUri(uri); }); const [retryCount, setRetryCount] = React.useState(0); const lastAttemptUri = React.useRef(null); React.useEffect(() => { const strictResolved = tryResolveImageUri(uri); setResolvedUri(strictResolved || (fallbackMode === 'none' ? '' : selectedFallback)); setShowPlaceholder(fallbackMode === 'none' && !strictResolved); setRetryCount(0); lastAttemptUri.current = strictResolved; }, [uri, fallbackMode, selectedFallback]); if (fallbackMode === 'none' && showPlaceholder) { return ( {getPlaceholderInitial(placeholderLabel)} ); } return ( { onError?.(event); const currentUri = resolvedUri || selectedFallback; // Smart Retry Logic for Wikimedia (first failure only) if (retryCount === 0 && currentUri.includes('upload.wikimedia.org')) { const fileName = getWikimediaFilePathFromThumbnailUrl(currentUri); if (fileName) { const redirectUrl = `https://commons.wikimedia.org/wiki/Special:FilePath/${encodeURIComponent(fileName)}`; setRetryCount(1); setResolvedUri(redirectUrl); lastAttemptUri.current = redirectUrl; return; } } const sourceFallbackUri = getPlantImageSourceFallbackUri(uri); if (sourceFallbackUri && sourceFallbackUri !== currentUri && lastAttemptUri.current !== sourceFallbackUri) { setResolvedUri(sourceFallbackUri); lastAttemptUri.current = sourceFallbackUri; return; } // If we get here, either it wasn't a Wikimedia URL, or filename extraction failed, // or the retry itself failed. if (fallbackMode === 'none') { setShowPlaceholder(true); return; } setResolvedUri((current) => { if (current === DEFAULT_PLANT_IMAGE_URI) return current; if (current === selectedFallback) return DEFAULT_PLANT_IMAGE_URI; return selectedFallback; }); // Prevent infinite loops if fallbacks also fail setRetryCount(current => current + 1); }} /> ); }; const styles = StyleSheet.create({ placeholder: { alignItems: 'center', justifyContent: 'center', backgroundColor: '#e7ece8', }, placeholderText: { fontSize: 18, fontWeight: '700', color: '#5f6f63', }, });