diff options
author | Samuel Newman <mozzius@protonmail.com> | 2024-04-22 23:39:32 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-04-22 23:39:32 +0100 |
commit | 76449fb6ef9b3eb327b6d059614d0da31c9d8e1f (patch) | |
tree | bca8fa797d3dabd5e0ca4f64a1b4f6a53f0efaa4 /src | |
parent | 1a4e05e9f99b479c4704b77bbf6c7551b0c0886b (diff) | |
download | voidsky-76449fb6ef9b3eb327b6d059614d0da31c9d8e1f.tar.zst |
[GIFs] Replace GIPHY with Tenor (#3651)
* replace GIPHY with Tenor * remove "directly" wording * replace GIPHY wording * remove log
Diffstat (limited to 'src')
-rw-r--r-- | src/components/dialogs/GifSelect.tsx | 65 | ||||
-rw-r--r-- | src/lib/constants.ts | 14 | ||||
-rw-r--r-- | src/state/queries/giphy.ts | 280 | ||||
-rw-r--r-- | src/state/queries/tenor.ts | 177 | ||||
-rw-r--r-- | src/view/com/composer/Composer.tsx | 17 | ||||
-rw-r--r-- | src/view/com/composer/photos/SelectGifBtn.tsx | 2 |
6 files changed, 220 insertions, 335 deletions
diff --git a/src/components/dialogs/GifSelect.tsx b/src/components/dialogs/GifSelect.tsx index a8fe016d1..41612aa5d 100644 --- a/src/components/dialogs/GifSelect.tsx +++ b/src/components/dialogs/GifSelect.tsx @@ -5,7 +5,6 @@ import {BottomSheetFlatListMethods} from '@discord/bottom-sheet' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {GIPHY_PRIVACY_POLICY} from '#/lib/constants' import {logEvent} from '#/lib/statsig/statsig' import {cleanError} from '#/lib/strings/errors' import {isWeb} from '#/platform/detection' @@ -13,7 +12,11 @@ import { useExternalEmbedsPrefs, useSetExternalEmbedPref, } from '#/state/preferences' -import {Gif, useGifphySearch, useGiphyTrending} from '#/state/queries/giphy' +import { + Gif, + useFeaturedGifsQuery, + useGifSearchQuery, +} from '#/state/queries/tenor' import {ErrorScreen} from '#/view/com/util/error/ErrorScreen' import {ErrorBoundary} from '#/view/com/util/ErrorBoundary' import {atoms as a, useBreakpoints, useTheme} from '#/alf' @@ -22,7 +25,6 @@ import * as TextField from '#/components/forms/TextField' import {useThrottledValue} from '#/components/hooks/useThrottledValue' import {ArrowLeft_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow' import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2' -import {InlineLinkText} from '#/components/Link' import {Button, ButtonIcon, ButtonText} from '../Button' import {ListFooter, ListMaybePlaceholder} from '../Lists' import {Text} from '../Typography' @@ -46,14 +48,14 @@ export function GifSelectDialog({ let content = null let snapPoints - switch (externalEmbedsPrefs?.giphy) { + switch (externalEmbedsPrefs?.tenor) { case 'show': content = <GifList control={control} onSelectGif={onSelectGif} /> snapPoints = ['100%'] break case 'hide': default: - content = <GiphyConsentPrompt control={control} /> + content = <TenorConsentPrompt control={control} /> break } @@ -90,8 +92,8 @@ function GifList({ const isSearching = search.length > 0 - const trendingQuery = useGiphyTrending() - const searchQuery = useGifphySearch(search) + const trendingQuery = useFeaturedGifsQuery() + const searchQuery = useGifSearchQuery(search) const { data, @@ -105,17 +107,7 @@ function GifList({ } = isSearching ? searchQuery : trendingQuery const flattenedData = useMemo(() => { - const uniquenessSet = new Set<string>() - - function filter(gif: Gif) { - if (!gif) return false - if (uniquenessSet.has(gif.id)) { - return false - } - uniquenessSet.add(gif.id) - return true - } - return data?.pages.flatMap(page => page.data.filter(filter)) || [] + return data?.pages.flatMap(page => page.results) || [] }, [data]) const renderItem = useCallback( @@ -181,7 +173,7 @@ function GifList({ <TextField.Icon icon={Search} /> <TextField.Input label={_(msg`Search GIFs`)} - placeholder={_(msg`Powered by GIPHY`)} + placeholder={_(msg`Search Tenor`)} onChangeText={text => { setSearch(text) listRef.current?.scrollToOffset({offset: 0, animated: false}) @@ -223,12 +215,12 @@ function GifList({ emptyType="results" sideBorders={false} errorTitle={_(msg`Failed to load GIFs`)} - errorMessage={_(msg`There was an issue connecting to GIPHY.`)} + errorMessage={_(msg`There was an issue connecting to Tenor.`)} emptyMessage={ isSearching ? _(msg`No search results found for "${search}".`) : _( - msg`No trending GIFs found. There may be an issue with GIPHY.`, + msg`No featured GIFs found. There may be an issue with Tenor.`, ) } /> @@ -287,7 +279,9 @@ function GifPreview({ {aspectRatio: 1, opacity: pressed ? 0.8 : 1}, t.atoms.bg_contrast_25, ]} - source={{uri: gif.images.preview_gif.url}} + source={{ + uri: gif.media_formats.tinygif.url, + }} contentFit="cover" accessibilityLabel={gif.title} accessibilityHint="" @@ -299,61 +293,56 @@ function GifPreview({ ) } -function GiphyConsentPrompt({control}: {control: Dialog.DialogControlProps}) { +function TenorConsentPrompt({control}: {control: Dialog.DialogControlProps}) { const {_} = useLingui() const t = useTheme() const {gtMobile} = useBreakpoints() const setExternalEmbedPref = useSetExternalEmbedPref() const onShowPress = useCallback(() => { - setExternalEmbedPref('giphy', 'show') + setExternalEmbedPref('tenor', 'show') }, [setExternalEmbedPref]) const onHidePress = useCallback(() => { - setExternalEmbedPref('giphy', 'hide') + setExternalEmbedPref('tenor', 'hide') control.close() }, [control, setExternalEmbedPref]) const gtMobileWeb = gtMobile && isWeb return ( - <Dialog.ScrollableInner label={_(msg`Permission to use GIPHY`)}> + <Dialog.ScrollableInner label={_(msg`Permission to use Tenor`)}> <View style={a.gap_sm}> <Text style={[a.text_2xl, a.font_bold]}> - <Trans>Permission to use GIPHY</Trans> + <Trans>Permission to use Tenor</Trans> </Text> <View style={[a.mt_sm, a.mb_2xl, a.gap_lg]}> <Text> <Trans> - Bluesky uses GIPHY to provide the GIF selector feature. + Bluesky uses Tenor to provide the GIF selector feature. </Trans> </Text> <Text style={t.atoms.text_contrast_medium}> <Trans> - GIPHY may collect information about you and your device. You can - find out more in their{' '} - <InlineLinkText - to={GIPHY_PRIVACY_POLICY} - onPress={() => control.close()}> - privacy policy - </InlineLinkText> - . + Tenor is a third-party service that provides GIFs for use in + Bluesky. By enabling Tenor, requests will be made to Tenor's + servers to retrieve the GIFs. </Trans> </Text> </View> </View> <View style={[a.gap_md, gtMobileWeb && a.flex_row_reverse]}> <Button - label={_(msg`Enable GIPHY`)} + label={_(msg`Enable Tenor`)} onPress={onShowPress} onAccessibilityEscape={control.close} color="primary" size={gtMobileWeb ? 'small' : 'medium'} variant="solid"> <ButtonText> - <Trans>Enable GIPHY</Trans> + <Trans>Enable Tenor</Trans> </ButtonText> </Button> <Button diff --git a/src/lib/constants.ts b/src/lib/constants.ts index b96529b1f..bbfdbe27f 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -90,11 +90,9 @@ export const BSKY_FEED_OWNER_DIDS = [ 'did:plc:q6gjnaw2blty4crticxkmujt', ] -export const GIPHY_API_URL = 'https://api.giphy.com' -export const GIPHY_API_KEY = Platform.select({ - ios: 'ydVxhrQkwlcUjkVKx15mF6vyaNJbMeez', - android: 'Vwj3Ib7857dj3EcIg24Hiz1LbRVdGeYF', - default: 'vyL3hQQ8AipwcmIB8kFvg0NDs9faWg7G', -}) -export const GIPHY_PRIVACY_POLICY = - 'https://support.giphy.com/hc/en-us/articles/360032872931-GIPHY-Privacy-Policy' +export const GIF_SERVICE = 'https://gifs.bsky.app' + +export const GIF_SEARCH = (params: string) => + `${GIF_SERVICE}/tenor/v2/search?${params}` +export const GIF_FEATURED = (params: string) => + `${GIF_SERVICE}/tenor/v2/featured?${params}` diff --git a/src/state/queries/giphy.ts b/src/state/queries/giphy.ts deleted file mode 100644 index ca5ff65f5..000000000 --- a/src/state/queries/giphy.ts +++ /dev/null @@ -1,280 +0,0 @@ -import {keepPreviousData, useInfiniteQuery} from '@tanstack/react-query' - -import {GIPHY_API_KEY, GIPHY_API_URL} from '#/lib/constants' - -export const RQKEY_ROOT = 'giphy' -export const RQKEY_TRENDING = [RQKEY_ROOT, 'trending'] -export const RQKEY_SEARCH = (query: string) => [RQKEY_ROOT, 'search', query] - -const getTrendingGifs = createGiphyApi< - { - limit?: number - offset?: number - rating?: string - random_id?: string - bundle?: string - }, - {data: Gif[]; pagination: Pagination} ->('/v1/gifs/trending') - -const searchGifs = createGiphyApi< - { - q: string - limit?: number - offset?: number - rating?: string - lang?: string - random_id?: string - bundle?: string - }, - {data: Gif[]; pagination: Pagination} ->('/v1/gifs/search') - -export function useGiphyTrending() { - return useInfiniteQuery({ - queryKey: RQKEY_TRENDING, - queryFn: ({pageParam}) => getTrendingGifs({offset: pageParam}), - initialPageParam: 0, - getNextPageParam: lastPage => - lastPage.pagination.offset + lastPage.pagination.count, - }) -} - -export function useGifphySearch(query: string) { - return useInfiniteQuery({ - queryKey: RQKEY_SEARCH(query), - queryFn: ({pageParam}) => searchGifs({q: query, offset: pageParam}), - initialPageParam: 0, - getNextPageParam: lastPage => - lastPage.pagination.offset + lastPage.pagination.count, - enabled: !!query, - placeholderData: keepPreviousData, - }) -} - -function createGiphyApi<Input extends object, Ouput>( - path: string, -): (input: Input) => Promise< - Ouput & { - meta: Meta - } -> { - return async input => { - const url = new URL(path, GIPHY_API_URL) - url.searchParams.set('api_key', GIPHY_API_KEY) - - for (const [key, value] of Object.entries(input)) { - url.searchParams.set(key, String(value)) - } - - const res = await fetch(url.toString(), { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }) - if (!res.ok) { - throw new Error('Failed to fetch Giphy API') - } - return res.json() - } -} - -export type Gif = { - type: string - id: string - slug: string - url: string - bitly_url: string - embed_url: string - username: string - source: string - rating: string - content_url: string - user: User - source_tld: string - source_post_url: string - update_datetime: string - create_datetime: string - import_datetime: string - trending_datetime: string - images: Images - title: string - alt_text: string -} - -type Images = { - fixed_height: { - url: string - width: string - height: string - size: string - mp4: string - mp4_size: string - webp: string - webp_size: string - } - - fixed_height_still: { - url: string - width: string - height: string - } - - fixed_height_downsampled: { - url: string - width: string - height: string - size: string - webp: string - webp_size: string - } - - fixed_width: { - url: string - width: string - height: string - size: string - mp4: string - mp4_size: string - webp: string - webp_size: string - } - - fixed_width_still: { - url: string - width: string - height: string - } - - fixed_width_downsampled: { - url: string - width: string - height: string - size: string - webp: string - webp_size: string - } - - fixed_height_small: { - url: string - width: string - height: string - size: string - mp4: string - mp4_size: string - webp: string - webp_size: string - } - - fixed_height_small_still: { - url: string - width: string - height: string - } - - fixed_width_small: { - url: string - width: string - height: string - size: string - mp4: string - mp4_size: string - webp: string - webp_size: string - } - - fixed_width_small_still: { - url: string - width: string - height: string - } - - downsized: { - url: string - width: string - height: string - size: string - } - - downsized_still: { - url: string - width: string - height: string - } - - downsized_large: { - url: string - width: string - height: string - size: string - } - - downsized_medium: { - url: string - width: string - height: string - size: string - } - - downsized_small: { - mp4: string - width: string - height: string - mp4_size: string - } - - original: { - width: string - height: string - size: string - frames: string - mp4: string - mp4_size: string - webp: string - webp_size: string - } - - original_still: { - url: string - width: string - height: string - } - - looping: { - mp4: string - } - - preview: { - mp4: string - mp4_size: string - width: string - height: string - } - - preview_gif: { - url: string - width: string - height: string - } -} - -type User = { - avatar_url: string - banner_url: string - profile_url: string - username: string - display_name: string -} - -type Meta = { - msg: string - status: number - response_id: string -} - -type Pagination = { - offset: number - total_count: number - count: number -} diff --git a/src/state/queries/tenor.ts b/src/state/queries/tenor.ts new file mode 100644 index 000000000..66cfcec6a --- /dev/null +++ b/src/state/queries/tenor.ts @@ -0,0 +1,177 @@ +import {Platform} from 'react-native' +import {getLocales} from 'expo-localization' +import {keepPreviousData, useInfiniteQuery} from '@tanstack/react-query' + +import {GIF_FEATURED, GIF_SEARCH} from '#/lib/constants' + +export const RQKEY_ROOT = 'gif-service' +export const RQKEY_FEATURED = [RQKEY_ROOT, 'featured'] +export const RQKEY_SEARCH = (query: string) => [RQKEY_ROOT, 'search', query] + +const getTrendingGifs = createTenorApi(GIF_FEATURED) + +const searchGifs = createTenorApi<{q: string}>(GIF_SEARCH) + +export function useFeaturedGifsQuery() { + return useInfiniteQuery({ + queryKey: RQKEY_FEATURED, + queryFn: ({pageParam}) => getTrendingGifs({pos: pageParam}), + initialPageParam: undefined as string | undefined, + getNextPageParam: lastPage => lastPage.next, + }) +} + +export function useGifSearchQuery(query: string) { + return useInfiniteQuery({ + queryKey: RQKEY_SEARCH(query), + queryFn: ({pageParam}) => searchGifs({q: query, pos: pageParam}), + initialPageParam: undefined as string | undefined, + getNextPageParam: lastPage => lastPage.next, + enabled: !!query, + placeholderData: keepPreviousData, + }) +} + +function createTenorApi<Input extends object>( + urlFn: (params: string) => string, +): (input: Input & {pos?: string}) => Promise<{ + next: string + results: Gif[] +}> { + return async input => { + const params = new URLSearchParams() + + // set client key based on platform + 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') + + params.set( + 'media_filter', + (['preview', 'gif', 'tinygif'] satisfies ContentFormats[]).join(','), + ) + + const locale = getLocales?.()?.[0] + + if (locale) { + params.set('locale', locale.languageTag.replace('-', '_')) + + if (locale.regionCode) { + params.set('country', locale.regionCode) + } + } + + 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 Tenor API') + } + return res.json() + } +} + +export type Gif = { + /** + * A Unix timestamp that represents when this post was created. + */ + created: number + /** + * Returns true if this post contains audio. + * Note: Only video formats support audio. The GIF image file format can't contain audio information. + */ + hasaudio: boolean + /** + * Tenor result identifier + */ + id: string + /** + * A dictionary with a content format as the key and a Media Object as the value. + */ + media_formats: Record<ContentFormats, MediaObject> + /** + * An array of tags for the post + */ + tags: string[] + /** + * The title of the post + */ + title: string + /** + * A textual description of the content. + * We recommend that you use content_description for user accessibility features. + */ + content_description: string + /** + * The full URL to view the post on tenor.com. + */ + itemurl: string + /** + * Returns true if this post contains captions. + */ + hascaption: boolean + /** + * Comma-separated list to signify whether the content is a sticker or static image, has audio, or is any combination of these. If sticker and static aren't present, then the content is a GIF. A blank flags field signifies a GIF without audio. + */ + flags: string + /** + * The most common background pixel color of the content + */ + bg_color?: string + /** + * A short URL to view the post on tenor.com. + */ + url: string +} + +type MediaObject = { + /** + * A URL to the media source + */ + url: string + /** + * Width and height of the media in pixels + */ + dims: [number, number] + /** + * Represents the time in seconds for one loop of the content. If the content is static, the duration is set to 0. + */ + duration: number + /** + * Size of the file in bytes + */ + size: number +} + +type ContentFormats = + | 'preview' + | 'gif' + // | 'mediumgif' + | 'tinygif' +// | 'nanogif' +// | 'mp4' +// | 'loopedmp4' +// | 'tinymp4' +// | 'nanomp4' +// | 'webm' +// | 'tinywebm' +// | 'nanowebm' diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index c5472c913..93e2dc6b5 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -29,8 +29,8 @@ import { useLanguagePrefs, useLanguagePrefsApi, } from '#/state/preferences/languages' -import {Gif} from '#/state/queries/giphy' import {useProfileQuery} from '#/state/queries/profile' +import {Gif} from '#/state/queries/tenor' import {ThreadgateSetting} from '#/state/queries/threadgate' import {getAgent, useSession} from '#/state/session' import {useComposerControls} from '#/state/shell/composer' @@ -316,18 +316,19 @@ export const ComposePost = observer(function ComposePost({ }, []) const onSelectGif = useCallback( - (gif: Gif) => + (gif: Gif) => { setExtLink({ - uri: `${gif.url}?hh=${gif.images.original.height}&ww=${gif.images.original.width}`, + uri: `${gif.media_formats.gif.url}?hh=${gif.media_formats.gif.dims[0]}&ww=${gif.media_formats.gif.dims[1]}`, isLoading: true, meta: { - url: gif.url, - image: gif.images.original_still.url, + url: gif.media_formats.gif.url, + image: gif.media_formats.preview.url, likelyType: LikelyType.HTML, - title: `${gif.title} - Find & Share on GIPHY`, - description: `ALT: ${gif.alt_text}`, + title: gif.content_description, + description: `ALT: ${gif.content_description}`, }, - }), + }) + }, [setExtLink], ) diff --git a/src/view/com/composer/photos/SelectGifBtn.tsx b/src/view/com/composer/photos/SelectGifBtn.tsx index 31310fdc1..60cef9a19 100644 --- a/src/view/com/composer/photos/SelectGifBtn.tsx +++ b/src/view/com/composer/photos/SelectGifBtn.tsx @@ -4,7 +4,7 @@ import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {logEvent} from '#/lib/statsig/statsig' -import {Gif} from '#/state/queries/giphy' +import {Gif} from '#/state/queries/tenor' import {atoms as a, useTheme} from '#/alf' import {Button} from '#/components/Button' import {useDialogControl} from '#/components/Dialog' |