import { SyntheticEvent, useEffect, useRef, useState } from 'react'; import { findNodeHandle, Pressable } from 'react-native'; import FastImage, { ImageStyle as FastImageStyle } from 'react-native-fast-image'; import { SvgUri } from 'react-native-svg'; import { useNativeMergeStyles, useStyles } from '@cleartrip/ct-design-style-manager'; import EventCollectorProvider, { useMergeEvents, useTriggerEvents } from '@cleartrip/ct-design-event-propagation'; import { IImageProps } from './type'; import { isSvg } from './style'; import { INativeUIEvent } from '@cleartrip/ct-design-types'; import { isIOS } from '@cleartrip/ct-design-common-utils'; /** * Native implementation of `Image`. * * Migrated from yagami (aldenui) per `Migration.MD`: * - `FastImage` is the raster renderer (RN's built-in `Image` * doesn't cache or decode as well across large lists). * - `.svg` URLs are detected via `isSvg` and delegated to * `react-native-svg`'s `SvgUri`, which `FastImage` can't handle. * - When `onClick` is supplied the tree is wrapped in a * `Pressable` (to get press feedback + hit-slop) inside an * `EventCollectorProvider` so upstream collectors see the tap * through the event-propagation hook chain. * * No built-in fallback URL is shipped — callers provide their own * via the `fallbackUrl` prop when a product default exists. */ const Image = ({ src, alt, width, height, fallbackUrl, styleConfig, onClick, onLoad, onError, resizeMode, zoomTransition = false, }: IImageProps) => { const [imageSource, setImageSource] = useState<{ uri: string }>({ uri: src || fallbackUrl || '', }); const { root: customRootStyles = [] } = styleConfig || {}; const aspectRatio = customRootStyles.find((style) => style.aspectRatio); const nodeElement = useRef(null); const imageStyles = useStyles( () => ({ image: { height: aspectRatio ? {} : height, width, }, }), [height, width], ); const mergedImageStyles = useNativeMergeStyles( [imageStyles.image, ...customRootStyles], [imageStyles, customRootStyles], ); const clickWithNodeElement = (e: INativeUIEvent) => { if (zoomTransition && nodeElement.current) { e.nodeElement = nodeElement.current; } onClick?.(e); }; const mergedEvents = useMergeEvents({ onClick: clickWithNodeElement ? [clickWithNodeElement] : [] }); const { onClick: onClickEvent } = useTriggerEvents(mergedEvents); useEffect(() => { setImageSource({ uri: src || fallbackUrl || '' }); }, [fallbackUrl, src]); // `.svg` URLs go through react-native-svg — FastImage can't decode them. if (isSvg(src)) { return ; } const onImageError = () => { if (fallbackUrl) { setImageSource({ uri: fallbackUrl }); } if (onError) { // FastImage's error callback doesn't produce a DOM SyntheticEvent. // Synthesize the minimum shape the public API promises so // cross-platform consumers don't have to branch. const syntheticEvent = { currentTarget: { src: imageSource.uri, onerror: null, }, } as unknown as SyntheticEvent; onError(syntheticEvent as never); } }; const imageElement = ( ); if (!onClick) { return imageElement; } return ( { if (isIOS()) { nodeElement.current = findNodeHandle(ref); } }} style={mergedImageStyles} onPress={onClickEvent} > {imageElement} ); }; Image.displayName = 'Image'; export default Image;