2025-08-04 09:16:54 -07:00
|
|
|
// Copyright 2025 Signal Messenger, LLC
|
|
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
2025-08-05 11:13:10 -07:00
|
|
|
import React, { useMemo, useCallback, useState, useRef } from 'react';
|
2025-08-04 09:16:54 -07:00
|
|
|
|
2025-09-16 17:39:03 -07:00
|
|
|
import { computeBlurHashUrl } from '../util/computeBlurHashUrl.js';
|
2025-08-04 09:16:54 -07:00
|
|
|
|
|
|
|
export type Props = React.ImgHTMLAttributes<HTMLImageElement> &
|
|
|
|
Readonly<{
|
|
|
|
blurHash?: string;
|
|
|
|
alt: string;
|
|
|
|
intrinsicWidth?: number;
|
|
|
|
intrinsicHeight?: number;
|
|
|
|
}>;
|
|
|
|
|
|
|
|
export function ImageOrBlurhash({
|
|
|
|
src: imageSrc,
|
|
|
|
blurHash,
|
|
|
|
alt,
|
|
|
|
intrinsicWidth,
|
|
|
|
intrinsicHeight,
|
|
|
|
...rest
|
|
|
|
}: Props): JSX.Element {
|
2025-08-05 11:13:10 -07:00
|
|
|
const ref = useRef<HTMLImageElement | null>(null);
|
|
|
|
const [isLoaded, setIsLoaded] = useState(false);
|
|
|
|
|
2025-08-04 09:16:54 -07:00
|
|
|
const blurHashUrl = useMemo(() => {
|
|
|
|
return blurHash
|
|
|
|
? computeBlurHashUrl(blurHash, intrinsicWidth, intrinsicHeight)
|
|
|
|
: undefined;
|
|
|
|
}, [blurHash, intrinsicWidth, intrinsicHeight]);
|
|
|
|
|
2025-08-05 11:13:10 -07:00
|
|
|
const onLoad = useCallback(() => {
|
|
|
|
// Don't let background blurhash be visible at the same time as the image
|
|
|
|
// while React propagates the `isLoaded` change.
|
|
|
|
if (ref.current) {
|
|
|
|
ref.current.style.backgroundImage = 'none';
|
|
|
|
}
|
|
|
|
setIsLoaded(true);
|
|
|
|
}, [ref]);
|
|
|
|
|
2025-08-04 09:16:54 -07:00
|
|
|
const src = imageSrc ?? blurHashUrl;
|
|
|
|
return (
|
|
|
|
<img
|
|
|
|
{...rest}
|
2025-08-05 11:13:10 -07:00
|
|
|
ref={ref}
|
2025-08-04 09:16:54 -07:00
|
|
|
src={src}
|
|
|
|
alt={alt}
|
2025-08-05 11:13:10 -07:00
|
|
|
onLoad={onLoad}
|
2025-08-04 09:16:54 -07:00
|
|
|
style={{
|
|
|
|
// Use a background image with an data url of the blurhash which should
|
|
|
|
// show quickly and stay visible until the img src is loaded/decoded.
|
|
|
|
backgroundImage:
|
2025-08-05 11:13:10 -07:00
|
|
|
blurHashUrl != null && blurHashUrl !== src && !isLoaded
|
2025-08-04 09:16:54 -07:00
|
|
|
? `url(${blurHashUrl})`
|
|
|
|
: 'none',
|
2025-08-05 11:13:10 -07:00
|
|
|
aspectRatio:
|
|
|
|
intrinsicWidth && intrinsicHeight
|
|
|
|
? `${intrinsicWidth} / ${intrinsicHeight}`
|
|
|
|
: undefined,
|
2025-08-04 09:16:54 -07:00
|
|
|
|
2025-08-11 10:22:54 -07:00
|
|
|
width: '100%',
|
|
|
|
height: '100%',
|
|
|
|
|
2025-08-04 09:16:54 -07:00
|
|
|
// Preserve aspect ratio
|
|
|
|
backgroundSize: 'cover',
|
|
|
|
backgroundPosition: 'center',
|
|
|
|
}}
|
|
|
|
loading={blurHashUrl != null ? 'lazy' : 'eager'}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|