import { useState } from 'react';
import type { AppBskyEmbedGallery } from '@atcute/bluesky';
import { assignInlineVars } from '@vanilla-extract/dynamic';
import { clsx } from 'clsx';
import { useLargeAltBadgeEnabled } from '#/state/preferences/large-alt-badge';
import * as Dialog from '#/components/Dialog';
import { lightboxHandle, type LightboxPayload } from '#/components/dialogs/handles';
import * as styles from '#/components/ImageEmbed/AutoSizedImage.css';
import { getAspectRatio } from '#/components/ImageEmbed/carousel/utils';
import { MediaBadges } from '#/components/ImageEmbed/MediaBadges';
import ImageIcon from '#/icons/central/Images1_round_outlined_radius1_stroke2.svg';
export type AutoSizedImageProps = {
image: AppBskyEmbedGallery.ViewImage;
crop?: 'constrained' | 'none';
/** What the lightbox opens with; the image renders as a detached `Dialog.Trigger`. */
payload: LightboxPayload;
onPressIn?: () => void;
};
/**
* renders a single post image that preserves its aspect ratio and opens a lightbox on click.
*
* @param ratio determines the layout constraint: 'constrained' caps the height, 'none' does not cap the
* height.
*/
export function AutoSizedImage({ image, crop = 'constrained', payload, onPressIn }: AutoSizedImageProps) {
const [status, setStatus] = useState<'error' | 'loaded' | 'loading'>(image.thumbnail ? 'loading' : 'error');
const largeAlt = useLargeAltBadgeEnabled();
const measure = (node: HTMLImageElement | null) => {
if (node?.complete) {
setStatus(node.naturalWidth > 0 ? 'loaded' : 'error');
}
};
// Old images, or images from other clients, can lack an aspect ratio; those fall back to a square box.
const aspectRatio = getAspectRatio(image.aspectRatio);
// A ratio-less image is letterboxed inside its square box rather than cover-cropped to an unknown shape.
const isContain = aspectRatio === undefined;
const className = crop === 'none' ? styles.uncapped : styles.constrained;
const onPointerDown = onPressIn ? () => onPressIn() : undefined;
return (
<Dialog.Trigger
handle={lightboxHandle}
payload={payload}
type="button"
className={className}
style={assignInlineVars({ [styles.ratioVar]: String(aspectRatio ?? 1) })}
aria-label={image.alt || undefined}
onPointerDown={onPointerDown}
>
{status === 'error' ? (
<span className={styles.fallback}>
<ImageIcon className={styles.imageIcon} />
</span>
) : (
<img
className={clsx(
styles.image,
isContain && styles.imageContain,
status === 'loading' && styles.loading,
)}
src={image.thumbnail}
alt={image.alt}
loading="lazy"
onError={() => setStatus('error')}
onLoad={() => setStatus('loaded')}
ref={measure}
/>
)}
{/* A single image keeps its aspect ratio, so it's never cropped — only the alt badge applies. */}
<MediaBadges variant="single" hasAlt={!!image.alt} cropped={false} large={largeAlt} />
</Dialog.Trigger>
);
}