migrate from tenor to klipy

This commit is contained in:
vineyardbovines
2026-04-13 08:44:34 -04:00
parent 3ec24fcb22
commit 808ebd1379
3 changed files with 202 additions and 76 deletions
+5 -18
View File
@@ -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({
<TextField.Icon icon={Search} />
<TextField.Input
label={_(msg`Search GIFs`)}
placeholder={
useKlipy ? _(msg`Search KLIPY`) : _(msg`Search Tenor`)
}
placeholder={useKlipy ? _(msg`Search KLIPY`) : _(msg`Search Tenor`)}
onChangeText={text => {
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}
+177
View File
@@ -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<Input extends object>(
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[]
}
+20 -58
View File
@@ -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<Input extends object>(
},
})
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()')