import { useEffect, useRef, useState } from 'react';
import { assignInlineVars } from '@vanilla-extract/dynamic';
import { clsx } from 'clsx';
import { onVisibilityChange } from '#/lib/browser/visibility';
import type { GifEmbedParams } from '#/lib/media/gif-embed';
import { useAutoplayDisabled } from '#/state/preferences/autoplay';
import { PlayButtonIcon } from '#/components/PlayButtonIcon';
import * as Prompt from '#/components/Prompt';
import { Spinner } from '#/components/Spinner';
import { Text } from '#/components/Text';
import { m } from '#/paraglide/messages';
import * as styles from './GifEmbed.css';
export type GifEmbedProps = {
params: GifEmbedParams;
thumb: string | null;
altText: string;
isPreferredAltText: boolean;
hideAlt?: boolean;
className?: string;
};
/** Autoplaying GIF (tenor/klipy), rendered as a muted looping `<video>` with play/pause + GIF/ALT badges. */
export function GifEmbed({ params, thumb, altText, isPreferredAltText, hideAlt, className }: GifEmbedProps) {
const autoplayDisabled = useAutoplayDisabled();
const videoRef = useRef<HTMLVideoElement>(null);
const [isPlaying, setIsPlaying] = useState(!autoplayDisabled);
const [isLoaded, setIsLoaded] = useState(false);
// resume playback when the tab returns to the foreground.
useEffect(() => {
return onVisibilityChange((visible) => {
const video = videoRef.current;
if (visible && !autoplayDisabled && video?.paused) {
void video.play();
}
});
}, [autoplayDisabled]);
const onPress = () => {
const video = videoRef.current;
if (!video) {
return;
}
if (video.paused) {
void video.play();
} else {
video.pause();
}
};
const aspectRatio = params.dimensions.width / params.dimensions.height;
const resolvedAlt = !hideAlt && isPreferredAltText ? altText : undefined;
return (
<div className={clsx(styles.outer, className)}>
<div className={styles.box} style={assignInlineVars({ [styles.ratioVar]: String(aspectRatio) })}>
<div className={styles.inset}>
{!isPlaying && <div aria-hidden className={styles.dimInner} />}
<button
type="button"
className={styles.playButton}
aria-label={isPlaying ? m['common.gif.a11y.pause']() : m['common.gif.a11y.play']()}
onClick={onPress}
>
{!isLoaded ? (
<Spinner label={m['common.gif.loading']()} />
) : !isPlaying ? (
<PlayButtonIcon />
) : null}
</button>
<div className={styles.gifBadge}>
<Text size="xs" weight="bold" className={styles.badgeText}>
{m['common.gif.label']()}
</Text>
</div>
{resolvedAlt ? <AltBadge text={resolvedAlt} /> : null}
<video
ref={videoRef}
className={styles.video}
poster={thumb ?? undefined}
autoPlay={!autoplayDisabled ? true : undefined}
preload={!autoplayDisabled ? 'auto' : undefined}
playsInline
loop
muted
aria-label={altText}
onCanPlay={() => setIsLoaded(true)}
onPlay={() => setIsPlaying(true)}
onPause={() => setIsPlaying(false)}
>
{params.playerSources.map((source) => (
<source key={source.src} src={source.src} type={source.type} />
))}
</video>
{!isPlaying && <div aria-hidden className={styles.dimOuter} />}
</div>
</div>
</div>
);
}
function AltBadge({ text }: { text: string }) {
const handle = Prompt.usePromptHandle();
return (
<>
<button
type="button"
className={styles.altBadge}
aria-label={m['common.altText.show']()}
onClick={() => handle.open(null)}
>
<Text size="xs" weight="bold" className={styles.badgeText}>
{m['common.altText.badge']()}
</Text>
</button>
<Prompt.Outer handle={handle}>
<Prompt.TitleText>{m['common.altText.title']()}</Prompt.TitleText>
<Prompt.DescriptionText>{text}</Prompt.DescriptionText>
<Prompt.Actions>
<Prompt.Action onPress={() => {}} cta={m['common.action.close']()} color="secondary" />
</Prompt.Actions>
</Prompt.Outer>
</>
);
}