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