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