diff --git a/src/features/gifPicker/GifPickerDialog.tsx b/src/features/gifPicker/GifPickerDialog.tsx index af92a891b6..efd89edbf2 100644 --- a/src/features/gifPicker/GifPickerDialog.tsx +++ b/src/features/gifPicker/GifPickerDialog.tsx @@ -153,6 +153,7 @@ function GifPickerBody({ isLoading={!isRecentsActive && isPending} isError={!isRecentsActive && isError} isSearching={isSearching} + isRecentsEmpty={isRecentsActive} query={effectiveSearch} onRetry={refetch} onGoBack={onGoBack} diff --git a/src/features/gifPicker/components/GifAutocompleteSuggestions.tsx b/src/features/gifPicker/components/GifAutocompleteSuggestions.tsx deleted file mode 100644 index 5d5af6f115..0000000000 --- a/src/features/gifPicker/components/GifAutocompleteSuggestions.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import {Pressable, View} from 'react-native' -import {msg} from '@lingui/core/macro' -import {useLingui} from '@lingui/react' - -import {atoms as a, useTheme} from '#/alf' -import {MagnifyingGlass_Stroke2_Corner0_Rounded as SearchIcon} from '#/components/icons/MagnifyingGlass' -import {Text} from '#/components/Typography' - -const LISTBOX_ID = 'gif-autocomplete-listbox' - -export function suggestionItemId(index: number) { - return `gif-autocomplete-option-${index}` -} - -export {LISTBOX_ID as GIF_AUTOCOMPLETE_LISTBOX_ID} - -export function GifAutocompleteSuggestions({ - suggestions, - activeIndex, - onSelect, -}: { - suggestions: string[] - activeIndex: number - onSelect: (suggestion: string) => void -}) { - const {_} = useLingui() - const t = useTheme() - - if (suggestions.length === 0) return null - - return ( - - {suggestions.map((suggestion, index) => { - const isActive = index === activeIndex - return ( - onSelect(suggestion)} - style={state => [ - a.flex_row, - a.align_center, - a.gap_sm, - a.px_md, - a.py_sm, - (isActive || ('hovered' in state && state.hovered)) && - t.atoms.bg_contrast_25, - ]}> - - - {suggestion} - - - ) - })} - - ) -} diff --git a/src/features/gifPicker/components/GifCategoryPills.tsx b/src/features/gifPicker/components/GifCategoryPills.tsx index b3bec77f2e..d5aec3518c 100644 --- a/src/features/gifPicker/components/GifCategoryPills.tsx +++ b/src/features/gifPicker/components/GifCategoryPills.tsx @@ -4,19 +4,19 @@ import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {atoms as a} from '#/alf' -import {Button, ButtonIcon} from '#/components/Button' +import {Button, useSharedButtonTextStyles} from '#/components/Button' import {Celebrate_Stroke2_Corner0_Rounded as Celebrate} from '#/components/icons/Celebrate' -import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' import {Clock_Stroke2_Corner0_Rounded as Clock} from '#/components/icons/Clock' import {type Props as SVGIconProps} from '#/components/icons/common' import { - EmojiArc_Stroke2_Corner0_Rounded as EmojiArc, - EmojiHeartEyes_Stroke2_Corner0_Rounded as EmojiHeartEyes, EmojiSad_Stroke2_Corner0_Rounded as EmojiSad, EmojiSmile_Stroke2_Corner0_Rounded as EmojiSmile, } from '#/components/icons/Emoji' -import {Flame_Stroke2_Corner1_Rounded as Flame} from '#/components/icons/Flame' -import {Heart2_Filled_Stroke2_Corner0_Rounded as HeartFilled} from '#/components/icons/Heart2' +import {Heart2_Stroke2_Corner0_Rounded as Heart} from '#/components/icons/Heart2' +import {Shaka_Stroke2_Corner0_Rounded as Shaka} from '#/components/icons/Shaka' +import {Trending3_Stroke2_Corner1_Rounded as Trending} from '#/components/icons/Trending' + +const ICON_SIZE = 20 export type GifCategory = { id: string @@ -27,14 +27,12 @@ export type GifCategory = { export const GIF_CATEGORIES: readonly GifCategory[] = [ {id: 'recents', icon: Clock, label: msg`Recents`, searchterm: null}, - {id: 'trending', icon: Flame, label: msg`Trending`, searchterm: null}, - {id: 'love', icon: HeartFilled, label: msg`Love`, searchterm: 'love'}, + {id: 'trending', icon: Trending, label: msg`Trending`, searchterm: null}, + {id: 'love', icon: Heart, label: msg`Love`, searchterm: 'love'}, {id: 'happy', icon: EmojiSmile, label: msg`Happy`, searchterm: 'happy'}, {id: 'sad', icon: EmojiSad, label: msg`Sad`, searchterm: 'cry'}, {id: 'party', icon: Celebrate, label: msg`Party`, searchterm: 'congratulations'}, - {id: 'yes', icon: Check, label: msg`Yes`, searchterm: 'yes'}, - {id: 'lol', icon: EmojiArc, label: msg`LOL`, searchterm: 'lol'}, - {id: 'excited', icon: EmojiHeartEyes, label: msg`Excited`, searchterm: 'excited'}, + {id: 'yes', icon: Shaka, label: msg`Yes`, searchterm: 'yes'}, ] as const export function GifCategoryPills({ @@ -46,8 +44,9 @@ export function GifCategoryPills({ 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. + // Subscribe to locale changes so i18n._() returns the current translation. + // The lingui-msg-rule lint rule forbids _() with variables, so we use + // i18n._() directly to translate the MessageDescriptor from GIF_CATEGORIES. useLingui() return ( @@ -57,26 +56,36 @@ export function GifCategoryPills({ a.justify_between, a.align_center, a.gap_xs, - a.px_xl, - a.mb_sm, + a.mb_md, ]}> {GIF_CATEGORIES.map(category => { if (category.id === 'recents' && !hasRecents) return null const isActive = category.id === activeId - const label = i18n._(category.label) return ( ) })} ) } + +function PillIcon({icon: Icon}: {icon: React.ComponentType}) { + const textStyles = useSharedButtonTextStyles() + return ( + + ) +} diff --git a/src/features/gifPicker/components/GifPickerGrid.tsx b/src/features/gifPicker/components/GifPickerGrid.tsx index 60cc30ea87..a2ac8ad0fa 100644 --- a/src/features/gifPicker/components/GifPickerGrid.tsx +++ b/src/features/gifPicker/components/GifPickerGrid.tsx @@ -1,4 +1,4 @@ -import {forwardRef, useMemo} from 'react' +import {forwardRef} from 'react' import {useWindowDimensions, View} from 'react-native' import {cleanError} from '#/lib/strings/errors' @@ -38,10 +38,7 @@ export const GifPickerGrid = forwardRef( const {height} = useWindowDimensions() const numColumns = gtMobile ? 3 : 2 - const columns = useMemo( - () => distributeIntoColumns(items, numColumns), - [items, numColumns], - ) + const columns = distributeIntoColumns(items, numColumns) /** * The grid is a single FlatList row because the tiles are distributed @@ -49,7 +46,7 @@ export const GifPickerGrid = forwardRef( * the outer FlatList's scroll position, so pagination behaves the same * as a conventional grid. */ - const data = useMemo(() => (hasData ? [columns] : []), [hasData, columns]) + const data = hasData ? [columns] : [] return ( {!gtMobile && IS_WEB && ( @@ -66,8 +65,6 @@ export function GifPickerHeader({ }} /> - - {/* future: tabs (Trending / Recents / Categories) render here */} ) } diff --git a/src/features/gifPicker/components/GifPickerItem.tsx b/src/features/gifPicker/components/GifPickerItem.tsx index c69d8b4723..f7babef9f5 100644 --- a/src/features/gifPicker/components/GifPickerItem.tsx +++ b/src/features/gifPicker/components/GifPickerItem.tsx @@ -2,7 +2,7 @@ import {Image} from 'expo-image' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' -import {gifPreviewUrl} from '#/state/queries/tenor' +import {gifPreviewUrl} from '#/state/queries/gif' import {atoms as a, useTheme} from '#/alf' import {Button} from '#/components/Button' import {useAnalytics} from '#/analytics' diff --git a/src/features/gifPicker/components/GifPickerPlaceholder.tsx b/src/features/gifPicker/components/GifPickerPlaceholder.tsx index e478ded438..478d2aa386 100644 --- a/src/features/gifPicker/components/GifPickerPlaceholder.tsx +++ b/src/features/gifPicker/components/GifPickerPlaceholder.tsx @@ -7,6 +7,7 @@ export function GifPickerPlaceholder({ isLoading, isError, isSearching, + isRecentsEmpty, query, onRetry, onGoBack, @@ -14,12 +15,19 @@ export function GifPickerPlaceholder({ isLoading: boolean isError: boolean isSearching: boolean + isRecentsEmpty: boolean query: string onRetry: () => Promise onGoBack: () => void }) { const {_} = useLingui() + const emptyMessage = isSearching + ? _(msg`No GIFs found for "${query}".`) + : isRecentsEmpty + ? _(msg`No recent GIFs yet. Pick one to see it here.`) + : _(msg`No GIFs to show right now. Try again in a moment.`) + return ( ) } diff --git a/src/features/gifPicker/hooks/useGifAutocomplete.ts b/src/features/gifPicker/hooks/useGifAutocomplete.ts deleted file mode 100644 index d6af1f59cf..0000000000 --- a/src/features/gifPicker/hooks/useGifAutocomplete.ts +++ /dev/null @@ -1,109 +0,0 @@ -import {useRef, useState} from 'react' - -import {useKlipyAutocompleteQuery} from '#/state/queries/klipy' -import {useThrottledValue} from '#/components/hooks/useThrottledValue' -import {useAnalytics} from '#/analytics' -import {IS_WEB} from '#/env' - -export type GifAutocompleteState = { - /** The suggestion strings to display */ - suggestions: string[] - /** Whether the suggestion list should be visible */ - isVisible: boolean - /** Index of the keyboard-highlighted suggestion (web only), -1 = none */ - activeIndex: number - /** Call when the user selects a suggestion */ - selectSuggestion: (suggestion: string) => void - /** Call when the raw search text changes (from the input's onChangeText) */ - handleTextChange: (text: string) => void - /** Call with the key event from the search input (web only) */ - handleKeyDown: (key: string) => boolean - /** Call to dismiss suggestions (e.g. escape key) */ - dismiss: () => void -} - -export function useGifAutocomplete({ - onSelectSuggestion, -}: { - onSelectSuggestion: (text: string) => void -}): GifAutocompleteState { - const ax = useAnalytics() - // TODO: revert — hardcoded for local Klipy testing - const useKlipy = true // ax.features.enabled(ax.features.KlipyGifProviderEnable) - - const [rawText, setRawText] = useState('') - const [dismissed, setDismissed] = useState(false) - const justSelectedRef = useRef(false) - - const autocompleteQuery = useThrottledValue(rawText, 200) - const {data: suggestions} = useKlipyAutocompleteQuery(autocompleteQuery, { - enabled: useKlipy && !justSelectedRef.current, - }) - - const [activeIndex, setActiveIndex] = useState(-1) - - const isVisible = - rawText.length > 0 && - !dismissed && - !justSelectedRef.current && - (suggestions?.length ?? 0) > 0 - - const handleTextChange = (text: string) => { - setRawText(text) - if (justSelectedRef.current) { - justSelectedRef.current = false - } - setDismissed(false) - setActiveIndex(-1) - } - - const selectSuggestion = (suggestion: string) => { - justSelectedRef.current = true - setRawText(suggestion) - setActiveIndex(-1) - onSelectSuggestion(suggestion) - } - - const dismiss = () => { - setDismissed(true) - setActiveIndex(-1) - } - - const handleKeyDown = (key: string): boolean => { - if (!IS_WEB || !isVisible || !suggestions?.length) return false - - switch (key) { - case 'ArrowDown': { - setActiveIndex(i => (i + 1) % suggestions.length) - return true - } - case 'ArrowUp': { - setActiveIndex(i => (i <= 0 ? suggestions.length - 1 : i - 1)) - return true - } - case 'Enter': { - if (activeIndex >= 0 && activeIndex < suggestions.length) { - selectSuggestion(suggestions[activeIndex]) - return true - } - return false - } - case 'Escape': { - dismiss() - return true - } - default: - return false - } - } - - return { - suggestions: suggestions ?? [], - isVisible, - activeIndex, - selectSuggestion, - handleTextChange, - handleKeyDown, - dismiss, - } -} diff --git a/src/features/gifPicker/hooks/useGifPickerData.ts b/src/features/gifPicker/hooks/useGifPickerData.ts index fa0d58da52..0b55243c25 100644 --- a/src/features/gifPicker/hooks/useGifPickerData.ts +++ b/src/features/gifPicker/hooks/useGifPickerData.ts @@ -7,7 +7,6 @@ import { useTenorGifSearchQuery, } from '#/state/queries/tenor' import {useAnalytics} from '#/analytics' -import {type GifPickerProvider} from '#/features/gifPicker/types' /** * Single entry point for the GIF picker's data layer. Wraps the Klipy/Tenor @@ -24,7 +23,6 @@ export function useGifPickerData( // TODO: revert — hardcoded for local Klipy testing const useKlipy = true // ax.features.enabled(ax.features.KlipyGifProviderEnable) const isSearching = query.length > 0 - const provider: GifPickerProvider = useKlipy ? 'klipy' : 'tenor' const klipyFeatured = useKlipyFeaturedGifsQuery({ enabled: enabled && useKlipy && !isSearching, @@ -49,7 +47,6 @@ export function useGifPickerData( return { ...active, - provider, isSearching, } } diff --git a/src/features/gifPicker/hooks/useRecentGifs.ts b/src/features/gifPicker/hooks/useRecentGifs.ts index cd572deacb..a2230005bd 100644 --- a/src/features/gifPicker/hooks/useRecentGifs.ts +++ b/src/features/gifPicker/hooks/useRecentGifs.ts @@ -4,42 +4,37 @@ import {account} from '#/storage' const MAX_RECENT_GIFS = 20 +function readValid(did: string): Gif[] { + const stored = account.get([did, 'recentGifs']) ?? [] + // Earlier builds of this branch stored recents as JSON-serialized strings. + // Drop any malformed entries so a dev with stale local data doesn't crash. + if (stored.some(item => typeof item !== 'object' || item === null)) { + account.remove([did, 'recentGifs']) + return [] + } + return stored +} + 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 [] - } + return readValid(did) } 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) + const existing = readValid(did) + const deduped = existing.filter(g => g.id !== gif.id) + const updated = [gif, ...deduped].slice(0, MAX_RECENT_GIFS) account.set([did, 'recentGifs'], updated) } return { getRecents, addRecent, - hasRecents: did - ? (account.get([did, 'recentGifs'])?.length ?? 0) > 0 - : false, + hasRecents: did ? readValid(did).length > 0 : false, } } diff --git a/src/features/gifPicker/types.ts b/src/features/gifPicker/types.ts index 67e26bb793..f320f26b6d 100644 --- a/src/features/gifPicker/types.ts +++ b/src/features/gifPicker/types.ts @@ -1,3 +1 @@ -export {type Gif} from '#/state/queries/tenor' - -export type GifPickerProvider = 'klipy' | 'tenor' +export {type Gif} from '#/state/queries/gif' diff --git a/src/lib/api/resolve.ts b/src/lib/api/resolve.ts index 49b00fdfc0..4eb3259cae 100644 --- a/src/lib/api/resolve.ts +++ b/src/lib/api/resolve.ts @@ -26,7 +26,7 @@ import { } from '#/lib/strings/url-helpers' import {type ComposerImage} from '#/state/gallery' import {createComposerImage} from '#/state/gallery' -import {type Gif} from '#/state/queries/tenor' +import {type Gif} from '#/state/queries/gif' import {createGIFDescription} from '../gif-alt-text' type ResolvedExternalLink = { diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 1c66dfd14a..8364cd2f6c 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -182,8 +182,6 @@ export const GIF_KLIPY_SEARCH = (params: string) => `${GIF_SERVICE}/klipy/v2/search?${params}` export const GIF_KLIPY_FEATURED = (params: string) => `${GIF_SERVICE}/klipy/v2/featured?${params}` -export const GIF_KLIPY_AUTOCOMPLETE = (params: string) => - `${GIF_SERVICE}/klipy/v2/autocomplete?${params}` export const MAX_LABELERS = 20 diff --git a/src/state/queries/gif.ts b/src/state/queries/gif.ts new file mode 100644 index 0000000000..54fea78a03 --- /dev/null +++ b/src/state/queries/gif.ts @@ -0,0 +1,57 @@ +import {logger} from '#/logger' + +/** + * Rewrites a provider's CDN URL (Tenor or Klipy) to the corresponding + * bsky proxy hostname. Leaves unrecognized hosts untouched. + */ +export function gifPreviewUrl(gifUrl: string) { + try { + const url = new URL(gifUrl) + if (url.hostname === 'media.tenor.com') { + url.hostname = 't.gifs.bsky.app' + return url.href + } + if (url.hostname === 'static.klipy.com') { + url.hostname = 'k.gifs.bsky.app' + return url.href + } + return gifUrl + } catch (e) { + logger.debug('invalid url passed to gifPreviewUrl()') + return '' + } +} + +/** + * GIF shape returned by the Bluesky GIF proxy. The field names follow the + * Tenor schema; Klipy responses are normalized to the same shape by the + * proxy so downstream code can be provider-agnostic. + */ +export type Gif = { + created: number + hasaudio: boolean + id: string + media_formats: Record & + Partial> + tags: string[] + title: string + content_description: string + itemurl: string + hascaption: boolean + flags: string + bg_color?: string + url: string +} + +type MediaObject = { + url: string + dims: [number, number] + duration: number + size: number +} + +type BaseContentFormats = 'preview' | 'gif' | 'tinygif' + +type VideoContentFormats = 'mp4' | 'webm' + +export type ContentFormats = BaseContentFormats | VideoContentFormats diff --git a/src/state/queries/klipy.ts b/src/state/queries/klipy.ts index 588cc593c4..a99d35195e 100644 --- a/src/state/queries/klipy.ts +++ b/src/state/queries/klipy.ts @@ -1,66 +1,18 @@ import {Platform} from 'react-native' import {getLocales} from 'expo-localization' -import { - keepPreviousData, - useInfiniteQuery, - useQuery, -} from '@tanstack/react-query' +import {keepPreviousData, useInfiniteQuery} from '@tanstack/react-query' -import { - GIF_KLIPY_AUTOCOMPLETE, - GIF_KLIPY_FEATURED, - GIF_KLIPY_SEARCH, -} from '#/lib/constants' +import {GIF_KLIPY_FEATURED, GIF_KLIPY_SEARCH} from '#/lib/constants' import {logger} from '#/logger' -import {STALE} from '#/state/queries' -import {type Gif} from '#/state/queries/tenor' +import {type Gif} from '#/state/queries/gif' export const RQKEY_ROOT = 'klipy-gif-service' export const RQKEY_FEATURED = [RQKEY_ROOT, 'featured'] export const RQKEY_SEARCH = (query: string) => [RQKEY_ROOT, 'search', query] -export const RQKEY_AUTOCOMPLETE = (query: string) => [ - RQKEY_ROOT, - 'autocomplete', - query, -] const getTrendingGifs = createKlipyApi(GIF_KLIPY_FEATURED) const searchGifs = createKlipyApi<{q: string}>(GIF_KLIPY_SEARCH) -async function fetchKlipyAutocomplete(query: string): Promise { - const params = new URLSearchParams() - - params.set( - 'client_key', - Platform.select({ - ios: 'bluesky-ios', - android: 'bluesky-android', - default: 'bluesky-web', - }), - ) - - params.set('limit', '8') - - const locale = getLocales?.()?.[0] - if (locale) { - params.set('locale', locale.languageTag.replace('-', '_')) - } - - params.set('q', query) - - const res = await fetch(GIF_KLIPY_AUTOCOMPLETE(params.toString()), { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }) - if (!res.ok) { - throw new Error('Failed to fetch KLIPY autocomplete API') - } - const body: KlipyAutocompleteResponse = await res.json() - return body.results -} - export function useFeaturedGifsQuery(options?: {enabled?: boolean}) { return useInfiniteQuery({ queryKey: RQKEY_FEATURED, @@ -85,19 +37,6 @@ export function useGifSearchQuery( }) } -export function useKlipyAutocompleteQuery( - query: string, - options?: {enabled?: boolean}, -) { - return useQuery({ - queryKey: RQKEY_AUTOCOMPLETE(query), - queryFn: () => fetchKlipyAutocomplete(query), - enabled: query.length > 0 && options?.enabled !== false, - staleTime: STALE.HOURS.ONE, - placeholderData: keepPreviousData, - }) -} - function createKlipyApi( urlFn: (params: string) => string, ): (input: Input & {pos?: string}) => Promise<{ @@ -167,8 +106,3 @@ export function klipyUrlToBskyGifUrl(klipyUrl: string) { url.hostname = 'k.gifs.bsky.app' return url.href } - -type KlipyAutocompleteResponse = { - locale: string - results: string[] -} diff --git a/src/state/queries/resolve-link.ts b/src/state/queries/resolve-link.ts index b0f694b464..723d51c911 100644 --- a/src/state/queries/resolve-link.ts +++ b/src/state/queries/resolve-link.ts @@ -4,7 +4,7 @@ import {type QueryClient, useQuery} from '@tanstack/react-query' import {type ResolvedLink, resolveGif, resolveLink} from '#/lib/api/resolve' import {STALE} from '#/state/queries/index' import {useAgent} from '#/state/session' -import {type Gif} from './tenor' +import {type Gif} from './gif' export const RQKEY_LINK_ROOT = 'resolve-link' export const RQKEY_LINK = (url: string) => [RQKEY_LINK_ROOT, url] diff --git a/src/state/queries/tenor.ts b/src/state/queries/tenor.ts index 835f21f066..391fe0f44f 100644 --- a/src/state/queries/tenor.ts +++ b/src/state/queries/tenor.ts @@ -4,6 +4,7 @@ import {keepPreviousData, useInfiniteQuery} from '@tanstack/react-query' import {GIF_FEATURED, GIF_SEARCH} from '#/lib/constants' import {logger} from '#/logger' +import {type ContentFormats, type Gif} from '#/state/queries/gif' export const RQKEY_ROOT = 'gif-service' export const RQKEY_FEATURED = [RQKEY_ROOT, 'featured'] @@ -102,117 +103,3 @@ export function tenorUrlToBskyGifUrl(tenorUrl: string) { url.hostname = 't.gifs.bsky.app' return url.href } - -/** - * Returns the appropriate URL for a GIF preview image. - * Tenor URLs (media.tenor.com) are routed through t.gifs.bsky.app; - * KLIPY URLs (static.klipy.com) are routed through k.gifs.bsky.app. - */ -export function gifPreviewUrl(gifUrl: string) { - try { - const url = new URL(gifUrl) - if (url.hostname === 'media.tenor.com') { - url.hostname = 't.gifs.bsky.app' - return url.href - } - if (url.hostname === 'static.klipy.com') { - url.hostname = 'k.gifs.bsky.app' - return url.href - } - return gifUrl - } catch (e) { - logger.debug('invalid url passed to gifPreviewUrl()') - return '' - } -} - -export type Gif = { - /** - * A Unix timestamp that represents when this post was created. - */ - created: number - /** - * Returns true if this post contains audio. - * Note: Only video formats support audio. The GIF image file format can't contain audio information. - */ - hasaudio: boolean - /** - * Tenor result identifier - */ - id: string - /** - * A dictionary with a content format as the key and a Media Object as the value. - */ - media_formats: Record & - Partial> - /** - * An array of tags for the post - */ - tags: string[] - /** - * The title of the post - */ - title: string - /** - * A textual description of the content. - * We recommend that you use content_description for user accessibility features. - */ - content_description: string - /** - * The full URL to view the post on tenor.com. - */ - itemurl: string - /** - * Returns true if this post contains captions. - */ - hascaption: boolean - /** - * Comma-separated list to signify whether the content is a sticker or static image, has audio, or is any combination of these. If sticker and static aren't present, then the content is a GIF. A blank flags field signifies a GIF without audio. - */ - flags: string - /** - * The most common background pixel color of the content - */ - bg_color?: string - /** - * A short URL to view the post on tenor.com. - */ - url: string -} - -type MediaObject = { - /** - * A URL to the media source - */ - url: string - /** - * Width and height of the media in pixels - */ - dims: [number, number] - /** - * Represents the time in seconds for one loop of the content. If the content is static, the duration is set to 0. - */ - duration: number - /** - * Size of the file in bytes - */ - size: number -} - -type BaseContentFormats = - | 'preview' - | 'gif' - // | 'mediumgif' - | 'tinygif' -// | 'nanogif' - -type VideoContentFormats = - | 'mp4' - // | 'loopedmp4' - // | 'tinymp4' - // | 'nanomp4' - | 'webm' -// | 'tinywebm' -// | 'nanowebm' - -type ContentFormats = BaseContentFormats | VideoContentFormats diff --git a/src/storage/schema.ts b/src/storage/schema.ts index e5cb225f9f..46032b7804 100644 --- a/src/storage/schema.ts +++ b/src/storage/schema.ts @@ -1,3 +1,4 @@ +import {type Gif} from '#/state/queries/gif' import {type ID as PolicyUpdate202508} from '#/components/PolicyUpdateOverlay/updates/202508/config' import {type Geolocation} from '#/geolocation/types' @@ -81,8 +82,7 @@ export type Account = { lastSelectedHomeFeed?: string /** - * Recently selected GIFs in the GIF picker, stored as serialized Gif objects. - * Most recent first, capped at 20. + * Recently selected GIFs in the GIF picker. Most recent first, capped at 20. */ - recentGifs?: string[] + recentGifs?: Gif[] } diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index d12b3a0bea..34c7066a1a 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -93,9 +93,9 @@ import { useLanguagePrefs, useLanguagePrefsApi, } from '#/state/preferences/languages' +import {type Gif} from '#/state/queries/gif' import {usePreferencesQuery} from '#/state/queries/preferences' import {useProfileQuery} from '#/state/queries/profile' -import {type Gif} from '#/state/queries/tenor' import {useAgent, useSession} from '#/state/session' import {useComposerControls} from '#/state/shell/composer' import {type ComposerOpts, type OnPostSuccessData} from '#/state/shell/composer' diff --git a/src/view/com/composer/ExternalEmbed.tsx b/src/view/com/composer/ExternalEmbed.tsx index 0db82a03f8..51a34b489f 100644 --- a/src/view/com/composer/ExternalEmbed.tsx +++ b/src/view/com/composer/ExternalEmbed.tsx @@ -2,11 +2,11 @@ import {useMemo} from 'react' import {type StyleProp, View, type ViewStyle} from 'react-native' import {cleanError} from '#/lib/strings/errors' +import {type Gif} from '#/state/queries/gif' import { useResolveGifQuery, useResolveLinkQuery, } from '#/state/queries/resolve-link' -import {type Gif} from '#/state/queries/tenor' import {ExternalEmbedRemoveBtn} from '#/view/com/composer/ExternalEmbedRemoveBtn' import {atoms as a, useTheme} from '#/alf' import {Loader} from '#/components/Loader' diff --git a/src/view/com/composer/GifAltText.tsx b/src/view/com/composer/GifAltText.tsx index e32df485c3..da215bb584 100644 --- a/src/view/com/composer/GifAltText.tsx +++ b/src/view/com/composer/GifAltText.tsx @@ -10,8 +10,8 @@ import { type EmbedPlayerParams, parseEmbedPlayerFromUrl, } from '#/lib/strings/embed-player' +import {type Gif} from '#/state/queries/gif' import {useResolveGifQuery} from '#/state/queries/resolve-link' -import {type Gif} from '#/state/queries/tenor' import {AltTextCounterWrapper} from '#/view/com/composer/AltTextCounterWrapper' import {atoms as a, useTheme} from '#/alf' import {Admonition} from '#/components/Admonition' diff --git a/src/view/com/composer/drafts/state/api.ts b/src/view/com/composer/drafts/state/api.ts index b07c290733..8109659689 100644 --- a/src/view/com/composer/drafts/state/api.ts +++ b/src/view/com/composer/drafts/state/api.ts @@ -10,7 +10,7 @@ import {getImageDim} from '#/lib/media/manip' import {mimeToExt} from '#/lib/media/video/util' import {shortenLinks} from '#/lib/strings/rich-text-manip' import {type ComposerImage} from '#/state/gallery' -import {type Gif} from '#/state/queries/tenor' +import {type Gif} from '#/state/queries/gif' import {threadgateAllowUISettingToAllowRecordValue} from '#/state/queries/threadgate/util' import {createPublicAgent} from '#/state/session/agent' import { diff --git a/src/view/com/composer/state/composer.ts b/src/view/com/composer/state/composer.ts index 75ea8153cf..17c2307d17 100644 --- a/src/view/com/composer/state/composer.ts +++ b/src/view/com/composer/state/composer.ts @@ -17,8 +17,8 @@ import { toBskyAppUrl, } from '#/lib/strings/url-helpers' import {type ComposerImage, createInitialImages} from '#/state/gallery' +import {type Gif} from '#/state/queries/gif' import {createPostgateRecord} from '#/state/queries/postgate/util' -import {type Gif} from '#/state/queries/tenor' import {threadgateRecordToAllowUISetting} from '#/state/queries/threadgate' import {type ThreadgateAllowUISetting} from '#/state/queries/threadgate' import {type ComposerOpts} from '#/state/shell/composer'