1import React from 'react'; 2import { View } from 'react-native-web'; 3 4import { ImageNativeProps, ImageSource, ImageLoadEventData } from './Image.types'; 5import AnimationManager, { AnimationManagerNode } from './web/AnimationManager'; 6import ImageWrapper from './web/ImageWrapper'; 7import loadStyle from './web/imageStyles'; 8import useSourceSelection from './web/useSourceSelection'; 9 10loadStyle(); 11 12export const ExpoImageModule = { 13 prefetch(urls: string | string[]): void { 14 const urlsArray = Array.isArray(urls) ? urls : [urls]; 15 urlsArray.forEach((url) => { 16 const img = new Image(); 17 img.src = url; 18 }); 19 }, 20 21 async clearMemoryCache(): Promise<boolean> { 22 return false; 23 }, 24 25 async clearDiskCache(): Promise<boolean> { 26 return false; 27 }, 28}; 29 30function onLoadAdapter(onLoad?: (event: ImageLoadEventData) => void) { 31 return (event: React.SyntheticEvent<HTMLImageElement, Event>) => { 32 const target = event.target as HTMLImageElement; 33 onLoad?.({ 34 source: { 35 url: target.currentSrc, 36 width: target.naturalWidth, 37 height: target.naturalHeight, 38 mediaType: null, 39 }, 40 cacheType: 'none', 41 }); 42 }; 43} 44 45function onErrorAdapter(onError?: { (event: { error: string }): void }) { 46 return ({ source }: { source?: ImageSource | null }) => { 47 onError?.({ 48 error: `Failed to load image from url: ${source?.uri}`, 49 }); 50 }; 51} 52 53// Used for some transitions to mimic native animations 54const setCssVariables = (element: HTMLElement, size: DOMRect) => { 55 element?.style.setProperty('--expo-image-width', `${size.width}px`); 56 element?.style.setProperty('--expo-image-height', `${size.height}px`); 57}; 58 59export default function ExpoImage({ 60 source, 61 placeholder, 62 contentFit, 63 contentPosition, 64 placeholderContentFit, 65 cachePolicy, 66 onLoad, 67 transition, 68 onError, 69 responsivePolicy, 70 onLoadEnd, 71 priority, 72 blurRadius, 73 recyclingKey, 74 style, 75 ...props 76}: ImageNativeProps) { 77 const imagePlaceholderContentFit = placeholderContentFit || 'scale-down'; 78 const blurhashStyle = { 79 objectFit: placeholderContentFit || contentFit, 80 }; 81 const { containerRef, source: selectedSource } = useSourceSelection( 82 source, 83 responsivePolicy, 84 setCssVariables 85 ); 86 87 const initialNodeAnimationKey = 88 (recyclingKey ? `${recyclingKey}-${placeholder?.[0]?.uri}` : placeholder?.[0]?.uri) ?? ''; 89 90 const initialNode: AnimationManagerNode | null = placeholder?.[0]?.uri 91 ? [ 92 initialNodeAnimationKey, 93 ({ onAnimationFinished }) => 94 (className, style) => ( 95 <ImageWrapper 96 {...props} 97 source={placeholder?.[0]} 98 style={{ 99 objectFit: imagePlaceholderContentFit, 100 ...(blurRadius ? { filter: `blur(${blurRadius}px)` } : {}), 101 ...style, 102 }} 103 className={className} 104 events={{ 105 onTransitionEnd: [onAnimationFinished], 106 }} 107 contentPosition={{ left: '50%', top: '50%' }} 108 hashPlaceholderContentPosition={contentPosition} 109 hashPlaceholderStyle={blurhashStyle} 110 /> 111 ), 112 ] 113 : null; 114 115 const currentNodeAnimationKey = 116 (recyclingKey 117 ? `${recyclingKey}-${selectedSource?.uri ?? placeholder?.[0]?.uri}` 118 : selectedSource?.uri ?? placeholder?.[0]?.uri) ?? ''; 119 120 const currentNode: AnimationManagerNode = [ 121 currentNodeAnimationKey, 122 ({ onAnimationFinished, onReady, onMount, onError: onErrorInner }) => 123 (className, style) => ( 124 <ImageWrapper 125 {...props} 126 source={selectedSource || placeholder?.[0]} 127 events={{ 128 onError: [onErrorAdapter(onError), onLoadEnd, onErrorInner], 129 onLoad: [onLoadAdapter(onLoad), onLoadEnd, onReady], 130 onMount: [onMount], 131 onTransitionEnd: [onAnimationFinished], 132 }} 133 style={{ 134 objectFit: selectedSource ? contentFit : imagePlaceholderContentFit, 135 ...(blurRadius ? { filter: `blur(${blurRadius}px)` } : {}), 136 ...style, 137 }} 138 className={className} 139 cachePolicy={cachePolicy} 140 priority={priority} 141 contentPosition={selectedSource ? contentPosition : { top: '50%', left: '50%' }} 142 hashPlaceholderContentPosition={contentPosition} 143 hashPlaceholderStyle={blurhashStyle} 144 accessibilityLabel={props.accessibilityLabel} 145 /> 146 ), 147 ]; 148 return ( 149 <View ref={containerRef} dataSet={{ expoimage: true }} style={[{ overflow: 'hidden' }, style]}> 150 <AnimationManager transition={transition} recyclingKey={recyclingKey} initial={initialNode}> 151 {currentNode} 152 </AnimationManager> 153 </View> 154 ); 155} 156