From 808ebd1379b12720757cbdb4a0a9e474cadc5def Mon Sep 17 00:00:00 2001 From: vineyardbovines Date: Mon, 13 Apr 2026 08:44:34 -0400 Subject: [PATCH] migrate from tenor to klipy --- src/components/dialogs/GifSelect.tsx | 23 +--- src/state/queries/klipy.ts | 177 +++++++++++++++++++++++++++ src/state/queries/tenor.ts | 78 +++--------- 3 files changed, 202 insertions(+), 76 deletions(-) create mode 100644 src/state/queries/klipy.ts diff --git a/src/components/dialogs/GifSelect.tsx b/src/components/dialogs/GifSelect.tsx index a939f4ab2d..ab99a9525b 100644 --- a/src/components/dialogs/GifSelect.tsx +++ b/src/components/dialogs/GifSelect.tsx @@ -14,20 +14,14 @@ import {Trans} from '@lingui/react/macro' import {cleanError} from '#/lib/strings/errors' import { - type Gif, -<<<<<<< Updated upstream - klipyStaticUrl, useFeaturedGifsQuery as useKlipyFeaturedGifsQuery, useGifSearchQuery as useKlipyGifSearchQuery, } from '#/state/queries/klipy' import { + type Gif, + gifPreviewUrl, useTenorFeaturedGifsQuery, useTenorGifSearchQuery, -======= - gifPreviewUrl, - useFeaturedGifsQuery, - useGifSearchQuery, ->>>>>>> Stashed changes } from '#/state/queries/tenor' import {ErrorScreen} from '#/view/com/util/error/ErrorScreen' import {ErrorBoundary} from '#/view/com/util/ErrorBoundary' @@ -107,7 +101,6 @@ function GifList({ const useKlipy = ax.features.enabled(ax.features.KlipyGifProviderEnable) const isSearching = search.length > 0 - const useKlipy = ax.features.enabled(ax.features.KlipyGifProviderEnable) const klipyTrending = useKlipyFeaturedGifsQuery({enabled: useKlipy}) const klipySearch = useKlipyGifSearchQuery(search, {enabled: useKlipy}) @@ -132,7 +125,7 @@ function GifList({ : tenorTrending const flattenedData = useMemo(() => { - return data?.pages.flatMap(page => page.data) || [] + return data?.pages.flatMap(page => page.results) || [] }, [data]) const renderItem = useCallback( @@ -188,9 +181,7 @@ function GifList({ { setSearch(text) listRef.current?.scrollToOffset({offset: 0, animated: false}) @@ -259,7 +250,7 @@ function GifList({ stickyHeaderIndices={[0]} onEndReached={onEndReached} onEndReachedThreshold={4} - keyExtractor={(item: Gif) => item.slug} + keyExtractor={(item: Gif) => item.id} keyboardDismissMode="on-drag" ListFooterComponent={ hasData ? ( @@ -339,11 +330,7 @@ export function GifPreview({ t.atoms.bg_contrast_25, ]} source={{ -<<<<<<< Updated upstream - uri: klipyStaticUrl(gif.file.sm.gif.url), -======= uri: gifPreviewUrl(gif.media_formats.tinygif.url), ->>>>>>> Stashed changes }} contentFit="cover" accessibilityLabel={gif.title} diff --git a/src/state/queries/klipy.ts b/src/state/queries/klipy.ts new file mode 100644 index 0000000000..7f385752a8 --- /dev/null +++ b/src/state/queries/klipy.ts @@ -0,0 +1,177 @@ +import {Platform} from 'react-native' +import {getLocales} from 'expo-localization' +import {keepPreviousData, useInfiniteQuery} from '@tanstack/react-query' + +import {GIF_KLIPY_FEATURED, GIF_KLIPY_SEARCH} from '#/lib/constants' +import {logger} from '#/logger' +import {type Gif} from '#/state/queries/tenor' + +export const RQKEY_ROOT = 'klipy-gif-service' +export const RQKEY_FEATURED = [RQKEY_ROOT, 'featured'] +export const RQKEY_SEARCH = (query: string) => [RQKEY_ROOT, 'search', query] + +const getTrendingGifs = createKlipyApi(GIF_KLIPY_FEATURED) +const searchGifs = createKlipyApi<{q: string}>(GIF_KLIPY_SEARCH) + +export function useFeaturedGifsQuery(options?: {enabled?: boolean}) { + return useInfiniteQuery({ + queryKey: RQKEY_FEATURED, + queryFn: ({pageParam}) => getTrendingGifs({pos: pageParam}), + initialPageParam: undefined as string | undefined, + getNextPageParam: lastPage => lastPage.next, + enabled: options?.enabled, + }) +} + +export function useGifSearchQuery( + query: string, + options?: {enabled?: boolean}, +) { + return useInfiniteQuery({ + queryKey: RQKEY_SEARCH(query), + queryFn: ({pageParam}) => searchGifs({q: query, pos: pageParam}), + initialPageParam: undefined as string | undefined, + getNextPageParam: lastPage => lastPage.next, + enabled: !!query && options?.enabled !== false, + placeholderData: keepPreviousData, + }) +} + +function createKlipyApi( + urlFn: (params: string) => string, +): (input: Input & {pos?: string}) => Promise<{ + next: string + results: Gif[] +}> { + return async input => { + const params = new URLSearchParams() + + params.set( + 'client_key', + Platform.select({ + ios: 'bluesky-ios', + android: 'bluesky-android', + default: 'bluesky-web', + }), + ) + + // 30 is divisible by 2 and 3, so both 2 and 3 column layouts can be used + params.set('limit', '30') + + params.set('contentfilter', 'high') + + const locale = getLocales?.()?.[0] + + if (locale) { + params.set('locale', locale.languageTag.replace('-', '_')) + } + + for (const [key, value] of Object.entries(input)) { + if (value !== undefined) { + params.set(key, String(value)) + } + } + + const res = await fetch(urlFn(params.toString()), { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }) + if (!res.ok) { + throw new Error('Failed to fetch KLIPY API') + } + const body: KlipyResponse = await res.json() + return { + next: body.next, + results: body.data.map(normalizeKlipyGif), + } + } +} + +/** + * Returns the static URL for a KLIPY GIF preview image. + * Currently a pass-through; will route through the bsky proxy once the + * backend proxy PR (tango) is deployed. + */ +export function klipyStaticUrl(gifUrl: string) { + try { + new URL(gifUrl) + return gifUrl + } catch (e) { + logger.debug('invalid url passed to klipyStaticUrl()') + return '' + } +} + +/** + * Maps a native KLIPY gif object onto the Tenor-shaped `Gif` type so + * downstream consumers (resolveGif, composer drafts, ExternalEmbed, etc.) + * continue to work unchanged. + * + * NOTE: the exact KLIPY response shape needs to be verified against the + * backend proxy once the tango PR is deployed — some fields here are + * best-effort based on what's documented today. + */ +function normalizeKlipyGif(k: KlipyGif): Gif { + const toMediaObject = (v: KlipyFileVariant) => ({ + url: v.url, + dims: [v.width, v.height] as [number, number], + duration: 0, + size: 0, + }) + + return { + id: k.slug, + title: k.title ?? '', + content_description: k.content_description ?? k.title ?? '', + tags: k.tags ?? [], + media_formats: { + gif: toMediaObject(k.file.hd.gif), + tinygif: toMediaObject(k.file.sm.gif), + preview: toMediaObject(k.file.sm.gif), + }, + // Tenor-only fields; stub sensibly for KLIPY. + created: 0, + hasaudio: false, + hascaption: false, + flags: '', + itemurl: k.url ?? '', + url: k.url ?? '', + } +} + +type KlipyFileVariant = { + url: string + width: number + height: number +} + +type KlipyFileFormats = { + gif: KlipyFileVariant + webp?: KlipyFileVariant + mp4?: KlipyFileVariant +} + +type KlipyFile = { + /** Small/thumbnail size, used in the picker grid */ + sm: KlipyFileFormats + /** Medium size */ + md: KlipyFileFormats + /** Full/large size, used when posting */ + hd: KlipyFileFormats +} + +type KlipyGif = { + slug: string + title?: string + content_description?: string + tags?: string[] + url?: string + file: KlipyFile +} + +type KlipyResponse = { + next: string + data: KlipyGif[] +} diff --git a/src/state/queries/tenor.ts b/src/state/queries/tenor.ts index 9522a801e0..dc802806e2 100644 --- a/src/state/queries/tenor.ts +++ b/src/state/queries/tenor.ts @@ -2,67 +2,37 @@ import {Platform} from 'react-native' import {getLocales} from 'expo-localization' import {keepPreviousData, useInfiniteQuery} from '@tanstack/react-query' -<<<<<<< Updated upstream -import {useAnalytics} from '#/analytics' -======= ->>>>>>> Stashed changes -import { - GIF_FEATURED, - GIF_KLIPY_FEATURED, - GIF_KLIPY_SEARCH, - GIF_SEARCH, -} from '#/lib/constants' +import {GIF_FEATURED, GIF_SEARCH} from '#/lib/constants' import {logger} from '#/logger' -import {useAnalytics} from '#/analytics' export const RQKEY_ROOT = 'gif-service' -export const RQKEY_FEATURED = (provider: string) => [ - RQKEY_ROOT, - 'featured', - provider, -] -export const RQKEY_SEARCH = (query: string, provider: string) => [ - RQKEY_ROOT, - 'search', - query, - provider, -] +export const RQKEY_FEATURED = [RQKEY_ROOT, 'featured'] +export const RQKEY_SEARCH = (query: string) => [RQKEY_ROOT, 'search', query] -const getTenorTrendingGifs = createTenorApi(GIF_FEATURED) -const searchTenorGifs = createTenorApi<{q: string}>(GIF_SEARCH) -const getKlipyTrendingGifs = createTenorApi(GIF_KLIPY_FEATURED) -const searchKlipyGifs = createTenorApi<{q: string}>(GIF_KLIPY_SEARCH) +const getTrendingGifs = createTenorApi(GIF_FEATURED) -export function useFeaturedGifsQuery() { - const ax = useAnalytics() - const useKlipy = ax.features.enabled(ax.features.KlipyGifProviderEnable) - const provider = useKlipy ? 'klipy' : 'tenor' +const searchGifs = createTenorApi<{q: string}>(GIF_SEARCH) +export function useTenorFeaturedGifsQuery(options?: {enabled?: boolean}) { return useInfiniteQuery({ - queryKey: RQKEY_FEATURED(provider), - queryFn: ({pageParam}) => - useKlipy - ? getKlipyTrendingGifs({pos: pageParam}) - : getTenorTrendingGifs({pos: pageParam}), + queryKey: RQKEY_FEATURED, + queryFn: ({pageParam}) => getTrendingGifs({pos: pageParam}), initialPageParam: undefined as string | undefined, getNextPageParam: lastPage => lastPage.next, + enabled: options?.enabled, }) } -export function useGifSearchQuery(query: string) { - const ax = useAnalytics() - const useKlipy = ax.features.enabled(ax.features.KlipyGifProviderEnable) - const provider = useKlipy ? 'klipy' : 'tenor' - +export function useTenorGifSearchQuery( + query: string, + options?: {enabled?: boolean}, +) { return useInfiniteQuery({ - queryKey: RQKEY_SEARCH(query, provider), - queryFn: ({pageParam}) => - useKlipy - ? searchKlipyGifs({q: query, pos: pageParam}) - : searchTenorGifs({q: query, pos: pageParam}), + queryKey: RQKEY_SEARCH(query), + queryFn: ({pageParam}) => searchGifs({q: query, pos: pageParam}), initialPageParam: undefined as string | undefined, getNextPageParam: lastPage => lastPage.next, - enabled: !!query, + enabled: !!query && options?.enabled !== false, placeholderData: keepPreviousData, }) } @@ -115,7 +85,7 @@ function createTenorApi( }, }) if (!res.ok) { - throw new Error('Failed to fetch GIF API') + throw new Error('Failed to fetch Tenor API') } return res.json() } @@ -134,14 +104,10 @@ export function tenorUrlToBskyGifUrl(tenorUrl: string) { } /** -<<<<<<< Updated upstream - * Returns the appropriate static URL for a GIF preview image. - * For Tenor URLs, rewrites through the bsky proxy. - * For KLIPY URLs, returns as-is (no proxy yet). -======= * Returns the appropriate URL for a GIF preview image. - * Rewrites Tenor URLs through the bsky proxy; KLIPY URLs pass through directly. ->>>>>>> Stashed changes + * Rewrites Tenor URLs through the bsky proxy; KLIPY URLs pass through + * directly (will route through the bsky proxy once the tango backend-proxy + * PR is deployed). */ export function gifPreviewUrl(gifUrl: string) { try { @@ -149,10 +115,6 @@ export function gifPreviewUrl(gifUrl: string) { if (url.hostname === 'media.tenor.com') { return tenorUrlToBskyGifUrl(gifUrl) } -<<<<<<< Updated upstream - // KLIPY static URLs and others pass through directly -======= ->>>>>>> Stashed changes return gifUrl } catch (e) { logger.debug('invalid url passed to gifPreviewUrl()')