migrate from tenor to klipy
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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
@@ -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()')
|
||||
|
||||
Reference in New Issue
Block a user