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
+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()')