feat(gif): recently-used GIFs in the picker
The GIF picker was a bare Giphy search grid with no memory of what you've sent, so re-sending a go-to reaction GIF meant re-typing the search every time. Add a "Recent" row at the top of the picker (default view; hidden while searching) for one-click re-sending. - New persisted state state/recentGifs.ts: recentGifsAtom (localStorage, cinny_recent_gifs_v1, getOnInit) + pure addRecentGif (dedupe-by-url move-to-front, cap 16, ignore empty url), with 5 unit tests. - GifPicker records every sent GIF (from search or the Recent row) to the front, and renders a 3-up thumbnail grid of recents above the search grid when there are recents and no active search term. Section label matches the picker's existing `// GIF_SEARCH` treatment (lotusTerminal) or a muted label otherwise. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
import React, { useCallback } 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 { 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))`;
|
||||
@@ -15,22 +17,108 @@ type GifPickerInnerProps = {
|
||||
lotusTerminal: boolean;
|
||||
};
|
||||
|
||||
// Small monospace section header matching the picker's `// GIF_SEARCH` treatment
|
||||
// (lotusTerminal) / a muted label otherwise.
|
||||
function SectionLabel({ text, lotusTerminal }: { text: string; lotusTerminal: boolean }) {
|
||||
if (lotusTerminal) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: '4px 2px',
|
||||
fontFamily: "'JetBrains Mono', 'Cascadia Code', monospace",
|
||||
fontSize: '10px',
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.1em',
|
||||
color: 'var(--lt-accent-orange)',
|
||||
userSelect: 'none',
|
||||
}}
|
||||
>
|
||||
{`// ${text.toUpperCase()}`}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: '2px 2px 4px',
|
||||
fontSize: '11px',
|
||||
fontWeight: 600,
|
||||
color: color.Surface.OnContainer,
|
||||
opacity: 0.6,
|
||||
userSelect: 'none',
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RecentGifs({
|
||||
recents,
|
||||
lotusTerminal,
|
||||
onPick,
|
||||
}: {
|
||||
recents: RecentGif[];
|
||||
lotusTerminal: boolean;
|
||||
onPick: (url: string, width: number, height: number) => void;
|
||||
}) {
|
||||
return (
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<SectionLabel text="Recent" lotusTerminal={lotusTerminal} />
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 4 }}>
|
||||
{recents.map((g) => (
|
||||
<button
|
||||
key={g.url}
|
||||
type="button"
|
||||
aria-label="Send recent GIF"
|
||||
onClick={() => onPick(g.url, g.width, g.height)}
|
||||
style={{
|
||||
padding: 0,
|
||||
border: 'none',
|
||||
background: 'transparent',
|
||||
cursor: 'pointer',
|
||||
height: 72,
|
||||
borderRadius: lotusTerminal ? '4px' : '8px',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={g.url}
|
||||
alt=""
|
||||
loading="lazy"
|
||||
style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function GifPickerInner({ onSelect, requestClose, lotusTerminal }: GifPickerInnerProps) {
|
||||
const { fetchGifs, searchKey } = React.useContext(SearchContext);
|
||||
const { fetchGifs, searchKey, term } = React.useContext(SearchContext);
|
||||
const [recents, setRecents] = useAtom(recentGifsAtom);
|
||||
|
||||
const sendGif = useCallback(
|
||||
(url: string, width: number, height: number) => {
|
||||
setRecents((prev) => addRecentGif(prev, { url, width, height }));
|
||||
onSelect(url, width, height);
|
||||
requestClose();
|
||||
},
|
||||
[onSelect, requestClose, setRecents],
|
||||
);
|
||||
|
||||
const handleClick = useCallback(
|
||||
(gif: IGif, e: React.SyntheticEvent) => {
|
||||
e.preventDefault();
|
||||
const r = gif.images.downsized ?? gif.images.original;
|
||||
const { url } = r;
|
||||
const width = Number(r.width) || 200;
|
||||
const height = Number(r.height) || 200;
|
||||
onSelect(url, width, height);
|
||||
requestClose();
|
||||
sendGif(r.url, Number(r.width) || 200, Number(r.height) || 200);
|
||||
},
|
||||
[onSelect, requestClose],
|
||||
[sendGif],
|
||||
);
|
||||
|
||||
const showRecents = recents.length > 0 && !(term ?? '').trim();
|
||||
|
||||
return (
|
||||
<Box direction="Column" style={{ width: PICKER_WIDTH_CSS }}>
|
||||
{lotusTerminal && (
|
||||
@@ -57,6 +145,9 @@ function GifPickerInner({ onSelect, requestClose, lotusTerminal }: GifPickerInne
|
||||
<div
|
||||
style={{ overflowY: 'auto', overflowX: 'hidden', maxHeight: '340px', padding: '0 8px 8px' }}
|
||||
>
|
||||
{showRecents && (
|
||||
<RecentGifs recents={recents} lotusTerminal={lotusTerminal} onPick={sendGif} />
|
||||
)}
|
||||
<Grid
|
||||
key={searchKey}
|
||||
fetchGifs={fetchGifs}
|
||||
|
||||
Reference in New Issue
Block a user