import { type ReactNode, useId, useState } from 'react';
import { MAX_ALT_TEXT } from '#/lib/constants/composer';
import type { Gif } from '#/lib/gif';
import { parseAltFromGIFDescription } from '#/lib/gif-alt-text';
import { type GifEmbedParams, parseGifEmbedFromUrl } from '#/lib/media/gif-embed';
import { getBlobUrl } from '#/lib/utils/blob-url';
import { trimText } from '#/lib/utils/text';
import { useResolveGifQuery } from '#/state/queries/resolve-link';
import * as Dialog from '#/components/Dialog';
import { GifEmbed } from '#/components/ExternalEmbed/GifEmbed';
import { Text } from '#/components/Text';
import * as TextField from '#/components/TextField';
import { Button, ButtonText } from '#/components/web/Button';
import { m } from '#/paraglide/messages';
import * as styles from './GifAltTextDialog.css';
type Props = {
altText: string;
gif: Gif;
handle: Dialog.DialogHandle;
onSubmit: (alt: string) => void;
};
export function GifAltTextDialog({ altText, gif, handle, onSubmit }: Props): ReactNode {
return (
<Dialog.Root disablePointerDismissal handle={handle}>
<Dialog.Popup scroll="body">
<DialogInner altText={altText} gif={gif} handle={handle} onSubmit={onSubmit} />
</Dialog.Popup>
</Dialog.Root>
);
}
function DialogInner({ altText, gif, handle, onSubmit }: Props): ReactNode {
const { data } = useResolveGifQuery(gif);
const thumb = getBlobUrl(data?.thumb?.source.blob);
const params = data ? parseGifEmbedFromUrl(data.uri) : undefined;
const vendorAltText = parseAltFromGIFDescription(data?.description ?? '').alt;
if (!params) {
return null;
}
return (
<GifAltTextForm
handle={handle}
initialAlt={altText || vendorAltText}
onSubmit={onSubmit}
params={params}
thumb={thumb}
vendorAltText={vendorAltText}
/>
);
}
type GifAltTextFormProps = {
handle: Dialog.DialogHandle;
initialAlt: string;
onSubmit: (alt: string) => void;
params: GifEmbedParams;
thumb: string | null;
vendorAltText: string;
};
const GifAltTextForm = ({
handle,
initialAlt,
onSubmit,
params,
thumb,
vendorAltText,
}: GifAltTextFormProps): ReactNode => {
const [altText, setAltText] = useState(initialAlt);
const counterId = useId();
const isOverLimit = altText.length > MAX_ALT_TEXT;
const canSave = altText !== initialAlt && !isOverLimit;
const counterLabel = isOverLimit
? m['view.composer.altText.charCountOverLimit']({ length: altText.length, max: MAX_ALT_TEXT })
: m['view.composer.altText.charCount']({ length: altText.length, max: MAX_ALT_TEXT });
const onSave = () => {
onSubmit(trimText(altText));
handle.close();
};
return (
<>
<Dialog.Header.Outer>
<Dialog.Header.Slot>
<Button
color="primary"
label={m['common.action.cancel']()}
onClick={() => handle.close()}
size="small"
variant="ghost"
>
<ButtonText size="md">{m['common.action.cancel']()}</ButtonText>
</Button>
</Dialog.Header.Slot>
<Dialog.Header.Content>
<Dialog.Header.TitleText>{m['view.composer.altText.action.add']()}</Dialog.Header.TitleText>
</Dialog.Header.Content>
<Dialog.Header.Slot>
<Button
className={canSave ? undefined : styles.inactiveSave}
color="primary"
disabled={!canSave}
label={m['common.action.save']()}
onClick={onSave}
size="small"
variant="ghost"
>
<ButtonText size="md">{m['common.action.save']()}</ButtonText>
</Button>
</Dialog.Header.Slot>
</Dialog.Header.Outer>
<Dialog.Body>
<div className={styles.gifBox}>
<GifEmbed altText={altText} hideAlt isPreferredAltText params={params} thumb={thumb} />
</div>
<div className={styles.form}>
<TextField.Root>
<TextField.LabelText
accessory={
<Text
aria-label={counterLabel}
className={styles.counter}
color={isOverLimit ? 'negative_500' : 'textContrastMedium'}
id={counterId}
size="sm"
>
{altText.length} / {MAX_ALT_TEXT}
</Text>
}
>
{m['view.composer.altText.descriptive']()}
</TextField.LabelText>
<TextField.Input
autoFocus
defaultValue={altText}
describedBy={counterId}
isInvalid={isOverLimit}
label={m['common.altText.label']()}
maxRows={8}
multiline
onChangeText={setAltText}
placeholder={vendorAltText || m['common.altText.label']()}
/>
</TextField.Root>
{/* announce only the crossing into over-limit while typing; a stable message avoids per-keystroke spam */}
<div className={styles.srOnly} role="status">
{isOverLimit ? m['view.composer.altText.error.overLimit']() : ''}
</div>
</div>
</Dialog.Body>
</>
);
};