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 {cleanError} from '#/lib/strings/errors'
|
||||||
import {
|
import {
|
||||||
type Gif,
|
|
||||||
<<<<<<< Updated upstream
|
|
||||||
klipyStaticUrl,
|
|
||||||
useFeaturedGifsQuery as useKlipyFeaturedGifsQuery,
|
useFeaturedGifsQuery as useKlipyFeaturedGifsQuery,
|
||||||
useGifSearchQuery as useKlipyGifSearchQuery,
|
useGifSearchQuery as useKlipyGifSearchQuery,
|
||||||
} from '#/state/queries/klipy'
|
} from '#/state/queries/klipy'
|
||||||
import {
|
import {
|
||||||
|
type Gif,
|
||||||
|
gifPreviewUrl,
|
||||||
useTenorFeaturedGifsQuery,
|
useTenorFeaturedGifsQuery,
|
||||||
useTenorGifSearchQuery,
|
useTenorGifSearchQuery,
|
||||||
=======
|
|
||||||
gifPreviewUrl,
|
|
||||||
useFeaturedGifsQuery,
|
|
||||||
useGifSearchQuery,
|
|
||||||
>>>>>>> Stashed changes
|
|
||||||
} from '#/state/queries/tenor'
|
} from '#/state/queries/tenor'
|
||||||
import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
|
import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
|
||||||
import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
|
import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
|
||||||
@@ -107,7 +101,6 @@ function GifList({
|
|||||||
const useKlipy = ax.features.enabled(ax.features.KlipyGifProviderEnable)
|
const useKlipy = ax.features.enabled(ax.features.KlipyGifProviderEnable)
|
||||||
|
|
||||||
const isSearching = search.length > 0
|
const isSearching = search.length > 0
|
||||||
const useKlipy = ax.features.enabled(ax.features.KlipyGifProviderEnable)
|
|
||||||
|
|
||||||
const klipyTrending = useKlipyFeaturedGifsQuery({enabled: useKlipy})
|
const klipyTrending = useKlipyFeaturedGifsQuery({enabled: useKlipy})
|
||||||
const klipySearch = useKlipyGifSearchQuery(search, {enabled: useKlipy})
|
const klipySearch = useKlipyGifSearchQuery(search, {enabled: useKlipy})
|
||||||
@@ -132,7 +125,7 @@ function GifList({
|
|||||||
: tenorTrending
|
: tenorTrending
|
||||||
|
|
||||||
const flattenedData = useMemo(() => {
|
const flattenedData = useMemo(() => {
|
||||||
return data?.pages.flatMap(page => page.data) || []
|
return data?.pages.flatMap(page => page.results) || []
|
||||||
}, [data])
|
}, [data])
|
||||||
|
|
||||||
const renderItem = useCallback(
|
const renderItem = useCallback(
|
||||||
@@ -188,9 +181,7 @@ function GifList({
|
|||||||
<TextField.Icon icon={Search} />
|
<TextField.Icon icon={Search} />
|
||||||
<TextField.Input
|
<TextField.Input
|
||||||
label={_(msg`Search GIFs`)}
|
label={_(msg`Search GIFs`)}
|
||||||
placeholder={
|
placeholder={useKlipy ? _(msg`Search KLIPY`) : _(msg`Search Tenor`)}
|
||||||
useKlipy ? _(msg`Search KLIPY`) : _(msg`Search Tenor`)
|
|
||||||
}
|
|
||||||
onChangeText={text => {
|
onChangeText={text => {
|
||||||
setSearch(text)
|
setSearch(text)
|
||||||
listRef.current?.scrollToOffset({offset: 0, animated: false})
|
listRef.current?.scrollToOffset({offset: 0, animated: false})
|
||||||
@@ -259,7 +250,7 @@ function GifList({
|
|||||||
stickyHeaderIndices={[0]}
|
stickyHeaderIndices={[0]}
|
||||||
onEndReached={onEndReached}
|
onEndReached={onEndReached}
|
||||||
onEndReachedThreshold={4}
|
onEndReachedThreshold={4}
|
||||||
keyExtractor={(item: Gif) => item.slug}
|
keyExtractor={(item: Gif) => item.id}
|
||||||
keyboardDismissMode="on-drag"
|
keyboardDismissMode="on-drag"
|
||||||
ListFooterComponent={
|
ListFooterComponent={
|
||||||
hasData ? (
|
hasData ? (
|
||||||
@@ -339,11 +330,7 @@ export function GifPreview({
|
|||||||
t.atoms.bg_contrast_25,
|
t.atoms.bg_contrast_25,
|
||||||
]}
|
]}
|
||||||
source={{
|
source={{
|
||||||
<<<<<<< Updated upstream
|
|
||||||
uri: klipyStaticUrl(gif.file.sm.gif.url),
|
|
||||||
=======
|
|
||||||
uri: gifPreviewUrl(gif.media_formats.tinygif.url),
|
uri: gifPreviewUrl(gif.media_formats.tinygif.url),
|
||||||
>>>>>>> Stashed changes
|
|
||||||
}}
|
}}
|
||||||
contentFit="cover"
|
contentFit="cover"
|
||||||
accessibilityLabel={gif.title}
|
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 {getLocales} from 'expo-localization'
|
||||||
import {keepPreviousData, useInfiniteQuery} from '@tanstack/react-query'
|
import {keepPreviousData, useInfiniteQuery} from '@tanstack/react-query'
|
||||||
|
|
||||||
<<<<<<< Updated upstream
|
import {GIF_FEATURED, GIF_SEARCH} from '#/lib/constants'
|
||||||
import {useAnalytics} from '#/analytics'
|
|
||||||
=======
|
|
||||||
>>>>>>> Stashed changes
|
|
||||||
import {
|
|
||||||
GIF_FEATURED,
|
|
||||||
GIF_KLIPY_FEATURED,
|
|
||||||
GIF_KLIPY_SEARCH,
|
|
||||||
GIF_SEARCH,
|
|
||||||
} from '#/lib/constants'
|
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {useAnalytics} from '#/analytics'
|
|
||||||
|
|
||||||
export const RQKEY_ROOT = 'gif-service'
|
export const RQKEY_ROOT = 'gif-service'
|
||||||
export const RQKEY_FEATURED = (provider: string) => [
|
export const RQKEY_FEATURED = [RQKEY_ROOT, 'featured']
|
||||||
RQKEY_ROOT,
|
export const RQKEY_SEARCH = (query: string) => [RQKEY_ROOT, 'search', query]
|
||||||
'featured',
|
|
||||||
provider,
|
|
||||||
]
|
|
||||||
export const RQKEY_SEARCH = (query: string, provider: string) => [
|
|
||||||
RQKEY_ROOT,
|
|
||||||
'search',
|
|
||||||
query,
|
|
||||||
provider,
|
|
||||||
]
|
|
||||||
|
|
||||||
const getTenorTrendingGifs = createTenorApi(GIF_FEATURED)
|
const getTrendingGifs = createTenorApi(GIF_FEATURED)
|
||||||
const searchTenorGifs = createTenorApi<{q: string}>(GIF_SEARCH)
|
|
||||||
const getKlipyTrendingGifs = createTenorApi(GIF_KLIPY_FEATURED)
|
|
||||||
const searchKlipyGifs = createTenorApi<{q: string}>(GIF_KLIPY_SEARCH)
|
|
||||||
|
|
||||||
export function useFeaturedGifsQuery() {
|
const searchGifs = createTenorApi<{q: string}>(GIF_SEARCH)
|
||||||
const ax = useAnalytics()
|
|
||||||
const useKlipy = ax.features.enabled(ax.features.KlipyGifProviderEnable)
|
|
||||||
const provider = useKlipy ? 'klipy' : 'tenor'
|
|
||||||
|
|
||||||
|
export function useTenorFeaturedGifsQuery(options?: {enabled?: boolean}) {
|
||||||
return useInfiniteQuery({
|
return useInfiniteQuery({
|
||||||
queryKey: RQKEY_FEATURED(provider),
|
queryKey: RQKEY_FEATURED,
|
||||||
queryFn: ({pageParam}) =>
|
queryFn: ({pageParam}) => getTrendingGifs({pos: pageParam}),
|
||||||
useKlipy
|
|
||||||
? getKlipyTrendingGifs({pos: pageParam})
|
|
||||||
: getTenorTrendingGifs({pos: pageParam}),
|
|
||||||
initialPageParam: undefined as string | undefined,
|
initialPageParam: undefined as string | undefined,
|
||||||
getNextPageParam: lastPage => lastPage.next,
|
getNextPageParam: lastPage => lastPage.next,
|
||||||
|
enabled: options?.enabled,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useGifSearchQuery(query: string) {
|
export function useTenorGifSearchQuery(
|
||||||
const ax = useAnalytics()
|
query: string,
|
||||||
const useKlipy = ax.features.enabled(ax.features.KlipyGifProviderEnable)
|
options?: {enabled?: boolean},
|
||||||
const provider = useKlipy ? 'klipy' : 'tenor'
|
) {
|
||||||
|
|
||||||
return useInfiniteQuery({
|
return useInfiniteQuery({
|
||||||
queryKey: RQKEY_SEARCH(query, provider),
|
queryKey: RQKEY_SEARCH(query),
|
||||||
queryFn: ({pageParam}) =>
|
queryFn: ({pageParam}) => searchGifs({q: query, pos: pageParam}),
|
||||||
useKlipy
|
|
||||||
? searchKlipyGifs({q: query, pos: pageParam})
|
|
||||||
: searchTenorGifs({q: query, pos: pageParam}),
|
|
||||||
initialPageParam: undefined as string | undefined,
|
initialPageParam: undefined as string | undefined,
|
||||||
getNextPageParam: lastPage => lastPage.next,
|
getNextPageParam: lastPage => lastPage.next,
|
||||||
enabled: !!query,
|
enabled: !!query && options?.enabled !== false,
|
||||||
placeholderData: keepPreviousData,
|
placeholderData: keepPreviousData,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -115,7 +85,7 @@ function createTenorApi<Input extends object>(
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error('Failed to fetch GIF API')
|
throw new Error('Failed to fetch Tenor API')
|
||||||
}
|
}
|
||||||
return res.json()
|
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.
|
* Returns the appropriate URL for a GIF preview image.
|
||||||
* Rewrites Tenor URLs through the bsky proxy; KLIPY URLs pass through directly.
|
* Rewrites Tenor URLs through the bsky proxy; KLIPY URLs pass through
|
||||||
>>>>>>> Stashed changes
|
* directly (will route through the bsky proxy once the tango backend-proxy
|
||||||
|
* PR is deployed).
|
||||||
*/
|
*/
|
||||||
export function gifPreviewUrl(gifUrl: string) {
|
export function gifPreviewUrl(gifUrl: string) {
|
||||||
try {
|
try {
|
||||||
@@ -149,10 +115,6 @@ export function gifPreviewUrl(gifUrl: string) {
|
|||||||
if (url.hostname === 'media.tenor.com') {
|
if (url.hostname === 'media.tenor.com') {
|
||||||
return tenorUrlToBskyGifUrl(gifUrl)
|
return tenorUrlToBskyGifUrl(gifUrl)
|
||||||
}
|
}
|
||||||
<<<<<<< Updated upstream
|
|
||||||
// KLIPY static URLs and others pass through directly
|
|
||||||
=======
|
|
||||||
>>>>>>> Stashed changes
|
|
||||||
return gifUrl
|
return gifUrl
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug('invalid url passed to gifPreviewUrl()')
|
logger.debug('invalid url passed to gifPreviewUrl()')
|
||||||
|
|||||||
Reference in New Issue
Block a user