diff --git a/__tests__/lib/string.test.ts b/__tests__/lib/string.test.ts index 7453b8d203..38966c35cb 100644 --- a/__tests__/lib/string.test.ts +++ b/__tests__/lib/string.test.ts @@ -8,6 +8,7 @@ import { parseStarterPackUri, } from '#/lib/strings/starter-pack' import {messages} from '#/locale/locales/en/messages' +import {klipyUrlToBskyGifUrl} from '#/state/queries/klipy' import {tenorUrlToBskyGifUrl} from '#/state/queries/tenor' import {cleanError} from '../../src/lib/strings/errors' import {createFullHandle, makeValidHandle} from '../../src/lib/strings/handles' @@ -450,6 +451,13 @@ describe('parseEmbedPlayerFromUrl', () => { 'https://sufjanstevens.bandcamp.com', 'https://bandcamp.com/', 'https://bandcamp.com', + + 'https://static.klipy.com/ii/abc123/73/ac/someFile.gif?hh=200&ww=300', + 'https://static.klipy.com/ii/abc123/73/ac/someFile.gif?hh=200&ww=300&mp4=videoSlugMp4&webm=videoSlugWebm', + 'https://static.klipy.com/ii/abc123/73/ac/someFile.gif?hh=200', + 'https://static.klipy.com/ii/abc123/73/ac/someFile.gif', + 'https://static.klipy.com/other/path.gif?hh=200&ww=300', + 'https://static.klipy.com', ] const outputs = [ @@ -845,6 +853,35 @@ describe('parseEmbedPlayerFromUrl', () => { undefined, undefined, undefined, + + { + type: 'klipy_gif', + source: 'klipy', + isGif: true, + hideDetails: true, + playerUri: 'https://k.gifs.bsky.app/ii/abc123/73/ac/someFile.gif', + dimensions: { + width: 300, + height: 200, + }, + }, + // With video slug params — on native (test env), keeps gif filename, + // strips mp4/webm params. On web, would swap to video filename. + { + type: 'klipy_gif', + source: 'klipy', + isGif: true, + hideDetails: true, + playerUri: 'https://k.gifs.bsky.app/ii/abc123/73/ac/someFile.gif', + dimensions: { + width: 300, + height: 200, + }, + }, + undefined, + undefined, + undefined, + undefined, ] it('correctly grabs the correct id from uri', () => { @@ -1049,3 +1086,31 @@ describe('tenorUrlToBskyGifUrl', () => { }, ) }) + +describe('klipyUrlToBskyGifUrl', () => { + const inputs = [ + 'https://static.klipy.com/ii/abc123/73/ac/someFile.gif', + 'https://static.klipy.com/ii/abc123/73/ac/someFile.gif?hh=200&ww=300', + ] + + it.each(inputs)( + 'returns url with k.gifs.bsky.app as hostname for input url', + input => { + const out = klipyUrlToBskyGifUrl(input) + expect(out.startsWith('https://k.gifs.bsky.app/')).toEqual(true) + }, + ) + + it('preserves the path and query params when rewriting', () => { + const out = klipyUrlToBskyGifUrl( + 'https://static.klipy.com/ii/abc123/73/ac/someFile.gif?hh=200&ww=300', + ) + expect(out).toEqual( + 'https://k.gifs.bsky.app/ii/abc123/73/ac/someFile.gif?hh=200&ww=300', + ) + }) + + it('returns empty string for invalid URLs', () => { + expect(klipyUrlToBskyGifUrl('not-a-url')).toEqual('') + }) +}) diff --git a/src/analytics/features/types.ts b/src/analytics/features/types.ts index 70cb8fc38d..36a354bb49 100644 --- a/src/analytics/features/types.ts +++ b/src/analytics/features/types.ts @@ -13,7 +13,7 @@ export enum Features { ImageUploadsBlobSize2mbEnabled = 'image_uploads:blob_size_2mb:enabled', GroupChatsEnable = 'group_chats:enable', DmsNewMessageComposerEnable = 'dms:new_message_composer:enable', + KlipyGifProviderEnable = 'klipy_gif_provider:enable', PostGalleryEmbedEnable = 'post_gallery_embed:enable', - AATest = 'aa-test', } diff --git a/src/components/MediaPreview.tsx b/src/components/MediaPreview.tsx index a6e30c820c..530c85ae66 100644 --- a/src/components/MediaPreview.tsx +++ b/src/components/MediaPreview.tsx @@ -3,7 +3,7 @@ import {Image} from 'expo-image' import {type AppBskyFeedDefs} from '@atproto/api' import {Trans} from '@lingui/react/macro' -import {isTenorGifUri} from '#/lib/strings/embed-player' +import {isGifEmbed} from '#/lib/strings/embed-player' import {atoms as a, useTheme} from '#/alf' import {MediaInsetBorder} from '#/components/MediaInsetBorder' import {Text} from '#/components/Typography' @@ -38,7 +38,7 @@ export function Embed({ ) } else if (e.type === 'link') { if (!e.view.external.thumb) return null - if (!isTenorGifUri(e.view.external.uri)) return null + if (!isGifEmbed(e.view.external.uri)) return null return ( diff --git a/src/components/dialogs/GifSelect.tsx b/src/components/dialogs/GifSelect.tsx index dfa159b2fe..bc58cbf137 100644 --- a/src/components/dialogs/GifSelect.tsx +++ b/src/components/dialogs/GifSelect.tsx @@ -8,16 +8,18 @@ import { import {type TextInput, View} from 'react-native' import {useWindowDimensions} from 'react-native' import {Image} from 'expo-image' -import {msg} from '@lingui/core/macro' -import {useLingui} from '@lingui/react' -import {Trans} from '@lingui/react/macro' +import {Trans, useLingui} from '@lingui/react/macro' import {cleanError} from '#/lib/strings/errors' +import { + useFeaturedGifsQuery as useKlipyFeaturedGifsQuery, + useGifSearchQuery as useKlipyGifSearchQuery, +} from '#/state/queries/klipy' import { type Gif, - tenorUrlToBskyGifUrl, - useFeaturedGifsQuery, - useGifSearchQuery, + gifPreviewUrl, + useTenorFeaturedGifsQuery, + useTenorGifSearchQuery, } from '#/state/queries/tenor' import {ErrorScreen} from '#/view/com/util/error/ErrorScreen' import {ErrorBoundary} from '#/view/com/util/ErrorBoundary' @@ -85,7 +87,8 @@ function GifList({ control: Dialog.DialogControlProps onSelectGif: (gif: Gif) => void }) { - const {_} = useLingui() + const ax = useAnalytics() + const {t: l} = useLingui() const t = useTheme() const {gtMobile} = useBreakpoints() const textInputRef = useRef(null) @@ -93,11 +96,14 @@ function GifList({ const [undeferredSearch, setSearch] = useState('') const search = useThrottledValue(undeferredSearch, 500) const {height} = useWindowDimensions() + const klipyEnabled = ax.features.enabled(ax.features.KlipyGifProviderEnable) const isSearching = search.length > 0 - const trendingQuery = useFeaturedGifsQuery() - const searchQuery = useGifSearchQuery(search) + const klipyTrending = useKlipyFeaturedGifsQuery({enabled: klipyEnabled}) + const klipySearch = useKlipyGifSearchQuery(search, {enabled: klipyEnabled}) + const tenorTrending = useTenorFeaturedGifsQuery({enabled: !klipyEnabled}) + const tenorSearch = useTenorGifSearchQuery(search, {enabled: !klipyEnabled}) const { data, @@ -108,7 +114,13 @@ function GifList({ isPending, isError, refetch, - } = isSearching ? searchQuery : trendingQuery + } = klipyEnabled + ? isSearching + ? klipySearch + : klipyTrending + : isSearching + ? tenorSearch + : tenorTrending const flattenedData = useMemo(() => { return data?.pages.flatMap(page => page.results) || [] @@ -158,7 +170,7 @@ function GifList({ color="secondary" shape="round" onPress={() => control.close()} - label={_(msg`Close GIF dialog`)}> + label={l`Close GIF dialog`}> )} @@ -166,8 +178,8 @@ function GifList({ { setSearch(text) listRef.current?.scrollToOffset({offset: 0, animated: false}) @@ -185,7 +197,7 @@ function GifList({ ) - }, [gtMobile, t.atoms.bg, _, control]) + }, [gtMobile, t.atoms.bg, l, control, klipyEnabled]) return ( <> @@ -212,14 +224,18 @@ function GifList({ emptyType="results" sideBorders={false} topBorder={false} - errorTitle={_(msg`Failed to load GIFs`)} - errorMessage={_(msg`There was an issue connecting to Tenor.`)} + errorTitle={l`Failed to load GIFs`} + errorMessage={ + klipyEnabled + ? l`There was an issue connecting to KLIPY.` + : l`There was an issue connecting to Tenor.` + } emptyMessage={ isSearching - ? _(msg`No search results found for "${search}".`) - : _( - msg`No featured GIFs found. There may be an issue with Tenor.`, - ) + ? l`No search results found for "${search}".` + : klipyEnabled + ? l`No featured GIFs found. There may be an issue with KLIPY.` + : l`No featured GIFs found. There may be an issue with Tenor.` } /> )} @@ -246,23 +262,19 @@ function GifList({ } function DialogError({details}: {details?: string}) { - const {_} = useLingui() + const {t: l} = useLingui() const control = Dialog.useDialogContext() return ( - +