diff --git a/src/features/gifPicker/GifPickerDialog.tsx b/src/features/gifPicker/GifPickerDialog.tsx index f8bfdfb241..af92a891b6 100644 --- a/src/features/gifPicker/GifPickerDialog.tsx +++ b/src/features/gifPicker/GifPickerDialog.tsx @@ -16,6 +16,7 @@ import {GifPickerGrid} from '#/features/gifPicker/components/GifPickerGrid' import {GifPickerHeader} from '#/features/gifPicker/components/GifPickerHeader' import {GifPickerPlaceholder} from '#/features/gifPicker/components/GifPickerPlaceholder' import {useGifPickerData} from '#/features/gifPicker/hooks/useGifPickerData' +import {useRecentGifs} from '#/features/gifPicker/hooks/useRecentGifs' import {type Gif} from '#/features/gifPicker/types' export function GifPickerDialog({ @@ -71,15 +72,18 @@ function GifPickerBody({ const [rawSearch, setRawSearch] = useState('') const [activeCategory, setActiveCategory] = useState('trending') const search = useThrottledValue(rawSearch, 750) + const {getRecents, addRecent, hasRecents} = useRecentGifs() // Determine the effective search query: // - If user is typing, use the typed text // - If a non-trending category is active, use its searchterm - // - Otherwise (trending), empty string triggers the featured endpoint + // - Otherwise (trending/recents), empty string triggers the featured endpoint const activeCategorySearchterm = GIF_CATEGORIES.find(c => c.id === activeCategory)?.searchterm ?? '' const effectiveSearch = search.length > 0 ? search : activeCategorySearchterm + const isRecentsActive = activeCategory === 'recents' && rawSearch.length === 0 + const { data, fetchNextPage, @@ -90,18 +94,20 @@ function GifPickerBody({ isError, isSearching, refetch, - } = useGifPickerData(effectiveSearch) + } = useGifPickerData(effectiveSearch, {enabled: !isRecentsActive}) - const items = data?.pages.flatMap(page => page.results) ?? [] + const networkItems = data?.pages.flatMap(page => page.results) ?? [] + const items = isRecentsActive ? getRecents() : networkItems const hasData = items.length > 0 const onEndReached = () => { + if (isRecentsActive) return if (isFetchingNextPage || !hasNextPage || error) return void fetchNextPage() } const onGoBack = () => { - if (isSearching) { + if (isSearching || activeCategory !== 'trending') { textInputRef.current?.clear() setRawSearch('') setActiveCategory('trending') @@ -120,6 +126,11 @@ function GifPickerBody({ listRef.current?.scrollToOffset({offset: 0, animated: false}) } + const handleSelectGif = (gif: Gif) => { + addRecent(gif) + onSelectGif(gif) + } + const showPills = rawSearch.length === 0 const header = ( @@ -134,12 +145,13 @@ function GifPickerBody({ )} {!hasData && ( ) diff --git a/src/features/gifPicker/components/GifCategoryPills.tsx b/src/features/gifPicker/components/GifCategoryPills.tsx index ffbc804a5a..ced1e070c5 100644 --- a/src/features/gifPicker/components/GifCategoryPills.tsx +++ b/src/features/gifPicker/components/GifCategoryPills.tsx @@ -10,10 +10,11 @@ export type GifCategory = { id: string emoji: string label: MessageDescriptor - searchterm: string | null // null = trending (uses featured endpoint) + searchterm: string | null // null = trending/recents (handled by consumer) } export const GIF_CATEGORIES: readonly GifCategory[] = [ + {id: 'recents', emoji: '🕐', label: msg`Recents`, searchterm: null}, {id: 'trending', emoji: '🔥', label: msg`Trending`, searchterm: null}, {id: 'love', emoji: '❤️', label: msg`Love`, searchterm: 'love'}, {id: 'happy', emoji: '😄', label: msg`Happy`, searchterm: 'happy'}, @@ -27,9 +28,11 @@ export const GIF_CATEGORIES: readonly GifCategory[] = [ export function GifCategoryPills({ activeId, onSelect, + hasRecents, }: { activeId: string onSelect: (category: GifCategory) => void + hasRecents: boolean }) { // useLingui() is called to re-render when the locale changes, even though we // translate via i18n._() below to satisfy the lingui-msg-rule lint constraint. @@ -42,6 +45,7 @@ export function GifCategoryPills({ showsHorizontalScrollIndicator={false} contentContainerStyle={[a.flex_row, a.gap_xs, a.px_xl]}> {GIF_CATEGORIES.map(category => { + if (category.id === 'recents' && !hasRecents) return null const isActive = category.id === activeId const label = i18n._(category.label) return ( @@ -52,10 +56,8 @@ export function GifCategoryPills({ size="small" variant={isActive ? 'solid' : 'outline'} color={isActive ? 'primary' : 'secondary'} - shape="default"> - - {category.emoji} {label} - + shape="round"> + {category.emoji} ) })} diff --git a/src/features/gifPicker/hooks/useRecentGifs.ts b/src/features/gifPicker/hooks/useRecentGifs.ts new file mode 100644 index 0000000000..cd572deacb --- /dev/null +++ b/src/features/gifPicker/hooks/useRecentGifs.ts @@ -0,0 +1,45 @@ +import {useSession} from '#/state/session' +import {type Gif} from '#/features/gifPicker/types' +import {account} from '#/storage' + +const MAX_RECENT_GIFS = 20 + +export function useRecentGifs() { + const {currentAccount} = useSession() + const did = currentAccount?.did + + const getRecents = (): Gif[] => { + if (!did) return [] + const stored = account.get([did, 'recentGifs']) + if (!stored) return [] + try { + return stored.map(s => JSON.parse(s) as Gif) + } catch { + return [] + } + } + + const addRecent = (gif: Gif) => { + if (!did) return + const stored = account.get([did, 'recentGifs']) ?? [] + // Remove duplicate if already in recents + const filtered = stored.filter(s => { + try { + return (JSON.parse(s) as Gif).id !== gif.id + } catch { + return true + } + }) + // Prepend and cap + const updated = [JSON.stringify(gif), ...filtered].slice(0, MAX_RECENT_GIFS) + account.set([did, 'recentGifs'], updated) + } + + return { + getRecents, + addRecent, + hasRecents: did + ? (account.get([did, 'recentGifs'])?.length ?? 0) > 0 + : false, + } +} diff --git a/src/storage/schema.ts b/src/storage/schema.ts index 2dd99ace2d..e5cb225f9f 100644 --- a/src/storage/schema.ts +++ b/src/storage/schema.ts @@ -79,4 +79,10 @@ export type Account = { birthdateLastUpdatedAt?: string lastSelectedHomeFeed?: string + + /** + * Recently selected GIFs in the GIF picker, stored as serialized Gif objects. + * Most recent first, capped at 20. + */ + recentGifs?: string[] }