1import React, { CSSProperties, SyntheticEvent, useEffect, Ref, useMemo } from 'react';
2
3import {
4  ImageContentPositionObject,
5  ImageContentPositionValue,
6  ImageNativeProps,
7  ImageSource,
8} from '../Image.types';
9import { useBlurhash } from '../utils/blurhash/useBlurhash';
10import { isBlurhashString, isThumbhashString } from '../utils/resolveSources';
11import { thumbHashStringToDataURL } from '../utils/thumbhash/thumbhash';
12
13function ensureUnit(value: string | number) {
14  const trimmedValue = String(value).trim();
15  if (trimmedValue.endsWith('%')) {
16    return trimmedValue;
17  }
18  return `${trimmedValue}px`;
19}
20
21type KeysOfUnion<T> = T extends T ? keyof T : never;
22
23function getObjectPositionFromContentPositionObject(
24  contentPosition?: ImageContentPositionObject
25): string {
26  const resolvedPosition = { ...contentPosition } as Record<
27    KeysOfUnion<ImageContentPositionObject>,
28    ImageContentPositionValue
29  >;
30  if (!resolvedPosition) {
31    return '50% 50%';
32  }
33  if (resolvedPosition.top == null && resolvedPosition.bottom == null) {
34    resolvedPosition.top = '50%';
35  }
36  if (resolvedPosition.left == null && resolvedPosition.right == null) {
37    resolvedPosition.left = '50%';
38  }
39
40  return (
41    ['top', 'bottom', 'left', 'right']
42      .map((key) => {
43        if (key in resolvedPosition) {
44          return `${key} ${ensureUnit(resolvedPosition[key])}`;
45        }
46        return '';
47      })
48      .join(' ') || '50% 50%'
49  );
50}
51
52function getFetchPriorityFromImagePriority(priority: ImageNativeProps['priority'] = 'normal') {
53  return priority && ['low', 'high'].includes(priority) ? priority : 'auto';
54}
55
56const ImageWrapper = React.forwardRef(
57  (
58    {
59      source,
60      events,
61      contentPosition,
62      hashPlaceholderContentPosition,
63      priority,
64      style,
65      hashPlaceholderStyle,
66      className,
67      accessibilityLabel,
68      ...props
69    }: {
70      source?: ImageSource | null;
71      events?: {
72        onLoad?: (((event: SyntheticEvent<HTMLImageElement, Event>) => void) | undefined | null)[];
73        onError?: ((({ source }: { source: ImageSource | null }) => void) | undefined | null)[];
74        onTransitionEnd?: ((() => void) | undefined | null)[];
75        onMount?: ((() => void) | undefined | null)[];
76      };
77      contentPosition?: ImageContentPositionObject;
78      hashPlaceholderContentPosition?: ImageContentPositionObject;
79      priority?: string | null;
80      style: CSSProperties;
81      hashPlaceholderStyle?: CSSProperties;
82      className?: string;
83      accessibilityLabel?: string;
84    },
85    ref: Ref<HTMLImageElement>
86  ) => {
87    useEffect(() => {
88      events?.onMount?.forEach((e) => e?.());
89    }, []);
90    const isBlurhash = isBlurhashString(source?.uri || '');
91    const isThumbhash = isThumbhashString(source?.uri || '');
92    const isHash = isBlurhash || isThumbhash;
93
94    // Thumbhash uri always has to start with 'thumbhash:/'
95    const thumbhash = source?.uri?.replace(/thumbhash:\//, '');
96    const thumbhashUri = useMemo(
97      () => (isThumbhash ? thumbHashStringToDataURL(thumbhash ?? '') : null),
98      [thumbhash]
99    );
100
101    const blurhashUri = useBlurhash(isBlurhash ? source?.uri : null, source?.width, source?.height);
102    const objectPosition = getObjectPositionFromContentPositionObject(
103      isHash ? hashPlaceholderContentPosition : contentPosition
104    );
105
106    const uri = isHash ? blurhashUri ?? thumbhashUri : source?.uri;
107    if (!uri) return null;
108    return (
109      <img
110        ref={ref}
111        alt={accessibilityLabel}
112        className={className}
113        src={uri || undefined}
114        key={source?.uri}
115        {...props}
116        style={{
117          width: '100%',
118          height: '100%',
119          position: 'absolute',
120          left: 0,
121          right: 0,
122          objectPosition,
123          ...style,
124          ...(isHash ? hashPlaceholderStyle : {}),
125        }}
126        // @ts-ignore
127        // eslint-disable-next-line react/no-unknown-property
128        fetchpriority={getFetchPriorityFromImagePriority(priority || 'normal')}
129        onLoad={(event) => {
130          if (typeof window !== 'undefined') {
131            // this ensures the animation will run, since the starting class is applied at least 1 frame before the target class set in the onLoad event callback
132            window.requestAnimationFrame(() => {
133              events?.onLoad?.forEach((e) => e?.(event));
134            });
135          } else {
136            events?.onLoad?.forEach((e) => e?.(event));
137          }
138        }}
139        onTransitionEnd={() => events?.onTransitionEnd?.forEach((e) => e?.())}
140        onError={() => events?.onError?.forEach((e) => e?.({ source: source || null }))}
141      />
142    );
143  }
144);
145export default ImageWrapper;
146