import React, { useCallback, useRef, useState } from 'react'; import FocusTrap from 'focus-trap-react'; import { useAtom } from 'jotai'; import { Grid, SearchBar, SearchContext, SearchContextManager } from '@giphy/react-components'; import { IGif } from '@giphy/js-types'; import { Box, color, config } from 'folds'; import { TapToSendBar } from './tap-to-send/TapToSendBar'; import { useRecentTouch } from '../hooks/useRecentTouch'; import { useElementSizeObserver } from '../hooks/useElementSizeObserver'; import { useSetting } from '../state/hooks/settings'; import { settingsAtom } from '../state/settings'; import { addRecentGif, RecentGif, recentGifsAtom } from '../state/recentGifs'; const PICKER_WIDTH = 312; const PICKER_WIDTH_CSS = `min(${PICKER_WIDTH}px, calc(100vw - 16px))`; type GifPickerInnerProps = { onSelect: (url: string, width: number, height: number) => void; requestClose: () => void; lotusTerminal: boolean; }; // Small monospace section header matching the picker's `// GIF_SEARCH` treatment // (lotusTerminal) / a muted label otherwise. const RECENT_LABEL_ID = 'gif-picker-recent-label'; function SectionLabel({ text, lotusTerminal, id, }: { text: string; lotusTerminal: boolean; id?: string; }) { if (lotusTerminal) { return (
{`// ${text.toUpperCase()}`}
); } return (
{text}
); } function RecentGifs({ recents, lotusTerminal, onPick, }: { recents: RecentGif[]; lotusTerminal: boolean; onPick: (gif: RecentGif) => void; }) { return (
{recents.map((g, i) => ( ))}
); } function GifPickerInner({ onSelect, requestClose, lotusTerminal }: GifPickerInnerProps) { const { fetchGifs, searchKey, term } = React.useContext(SearchContext); const [recents, setRecents] = useAtom(recentGifsAtom); const sendGif = useCallback( (gif: RecentGif) => { const { url, width, height, previewUrl } = gif; setRecents((prev) => addRecentGif(prev, { url, width, height, previewUrl })); onSelect(gif.url, gif.width, gif.height); requestClose(); }, [onSelect, requestClose, setRecents], ); // [Gitea #147] Touch: first tap parks the GIF in a preview bar, second tap // (or Send) sends. Mouse/keyboard/screen reader: one step, as before. const containerRef = useRef(null); const { wasTouch } = useRecentTouch(containerRef); const [pending, setPending] = useState<(RecentGif & { title?: string }) | undefined>(); const pick = useCallback( (gif: RecentGif & { title?: string }) => { if (wasTouch() && pending?.url !== gif.url) { setPending(gif); return; } setPending(undefined); sendGif(gif); }, [wasTouch, pending, sendGif], ); const handleClick = useCallback( (gif: IGif, e: React.SyntheticEvent) => { e.preventDefault(); const r = gif.images.downsized ?? gif.images.original; const previewUrl = gif.images.fixed_width_small_still?.url ?? gif.images.downsized_still?.url ?? gif.images.original_still?.url; pick({ url: r.url, width: Number(r.width) || 200, height: Number(r.height) || 200, previewUrl, title: gif.title, }); }, [pick], ); const showRecents = recents.length > 0 && !(term ?? '').trim(); // The container is min(312px, 100vw-16); feed the Grid the live pixel width // (minus the inner 8px padding on each side) so it doesn't overflow a phone // narrower than 312px with a fixed 296px grid. const [gridWidth, setGridWidth] = useState(PICKER_WIDTH - 16); useElementSizeObserver( useCallback(() => containerRef.current, []), useCallback((w) => setGridWidth(Math.max(1, Math.floor(w) - 16)), []), ); return ( {lotusTerminal && (
{'// GIF_SEARCH'}
)}
{pending && ( { setPending(undefined); sendGif(pending); }} onCancel={() => setPending(undefined)} /> )}
{showRecents && ( )}
); } type GifPickerProps = { apiKey: string; onSelect: (url: string, width: number, height: number) => void; requestClose: () => void; }; export function GifPicker({ apiKey, onSelect, requestClose }: GifPickerProps) { const [lotusTerminal] = useSetting(settingsAtom, 'lotusTerminal'); const [gifPickerEnabled] = useSetting(settingsAtom, 'gifPickerEnabled'); // Searches (and every keystroke) go straight to Giphy's API, so the picker // is opt-in (Settings → Messages) and must not render or fetch until then. if (!gifPickerEnabled) { return null; } const containerStyle = lotusTerminal ? { background: 'var(--lt-bg-secondary)', border: '1px solid color-mix(in srgb, var(--lt-accent-orange) 35%, transparent)', borderRadius: '4px', overflow: 'hidden', boxShadow: '0 4px 24px color-mix(in srgb, var(--lt-accent-orange) 10%, transparent), 0 0 0 1px color-mix(in srgb, var(--lt-accent-orange) 8%, transparent)', width: PICKER_WIDTH_CSS, } : { background: color.Surface.Container, border: `${config.borderWidth.B300} solid ${color.Surface.ContainerLine}`, borderRadius: config.radii.R400, overflow: 'hidden', boxShadow: color.Other.Shadow, width: PICKER_WIDTH_CSS, }; return ( ); }