[GIFs] Replace GIPHY with Tenor (#3651)
* replace GIPHY with Tenor * remove "directly" wording * replace GIPHY wording * remove logzio/stable
parent
1a4e05e9f9
commit
76449fb6ef
|
@ -5,7 +5,6 @@ import {BottomSheetFlatListMethods} from '@discord/bottom-sheet'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {GIPHY_PRIVACY_POLICY} from '#/lib/constants'
|
|
||||||
import {logEvent} from '#/lib/statsig/statsig'
|
import {logEvent} from '#/lib/statsig/statsig'
|
||||||
import {cleanError} from '#/lib/strings/errors'
|
import {cleanError} from '#/lib/strings/errors'
|
||||||
import {isWeb} from '#/platform/detection'
|
import {isWeb} from '#/platform/detection'
|
||||||
|
@ -13,7 +12,11 @@ import {
|
||||||
useExternalEmbedsPrefs,
|
useExternalEmbedsPrefs,
|
||||||
useSetExternalEmbedPref,
|
useSetExternalEmbedPref,
|
||||||
} from '#/state/preferences'
|
} 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 {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
|
||||||
import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
|
import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
|
||||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
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 {useThrottledValue} from '#/components/hooks/useThrottledValue'
|
||||||
import {ArrowLeft_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow'
|
import {ArrowLeft_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow'
|
||||||
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2'
|
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2'
|
||||||
import {InlineLinkText} from '#/components/Link'
|
|
||||||
import {Button, ButtonIcon, ButtonText} from '../Button'
|
import {Button, ButtonIcon, ButtonText} from '../Button'
|
||||||
import {ListFooter, ListMaybePlaceholder} from '../Lists'
|
import {ListFooter, ListMaybePlaceholder} from '../Lists'
|
||||||
import {Text} from '../Typography'
|
import {Text} from '../Typography'
|
||||||
|
@ -46,14 +48,14 @@ export function GifSelectDialog({
|
||||||
|
|
||||||
let content = null
|
let content = null
|
||||||
let snapPoints
|
let snapPoints
|
||||||
switch (externalEmbedsPrefs?.giphy) {
|
switch (externalEmbedsPrefs?.tenor) {
|
||||||
case 'show':
|
case 'show':
|
||||||
content = <GifList control={control} onSelectGif={onSelectGif} />
|
content = <GifList control={control} onSelectGif={onSelectGif} />
|
||||||
snapPoints = ['100%']
|
snapPoints = ['100%']
|
||||||
break
|
break
|
||||||
case 'hide':
|
case 'hide':
|
||||||
default:
|
default:
|
||||||
content = <GiphyConsentPrompt control={control} />
|
content = <TenorConsentPrompt control={control} />
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,8 +92,8 @@ function GifList({
|
||||||
|
|
||||||
const isSearching = search.length > 0
|
const isSearching = search.length > 0
|
||||||
|
|
||||||
const trendingQuery = useGiphyTrending()
|
const trendingQuery = useFeaturedGifsQuery()
|
||||||
const searchQuery = useGifphySearch(search)
|
const searchQuery = useGifSearchQuery(search)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
|
@ -105,17 +107,7 @@ function GifList({
|
||||||
} = isSearching ? searchQuery : trendingQuery
|
} = isSearching ? searchQuery : trendingQuery
|
||||||
|
|
||||||
const flattenedData = useMemo(() => {
|
const flattenedData = useMemo(() => {
|
||||||
const uniquenessSet = new Set<string>()
|
return data?.pages.flatMap(page => page.results) || []
|
||||||
|
|
||||||
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)) || []
|
|
||||||
}, [data])
|
}, [data])
|
||||||
|
|
||||||
const renderItem = useCallback(
|
const renderItem = useCallback(
|
||||||
|
@ -181,7 +173,7 @@ function GifList({
|
||||||
<TextField.Icon icon={Search} />
|
<TextField.Icon icon={Search} />
|
||||||
<TextField.Input
|
<TextField.Input
|
||||||
label={_(msg`Search GIFs`)}
|
label={_(msg`Search GIFs`)}
|
||||||
placeholder={_(msg`Powered by GIPHY`)}
|
placeholder={_(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})
|
||||||
|
@ -223,12 +215,12 @@ function GifList({
|
||||||
emptyType="results"
|
emptyType="results"
|
||||||
sideBorders={false}
|
sideBorders={false}
|
||||||
errorTitle={_(msg`Failed to load GIFs`)}
|
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={
|
emptyMessage={
|
||||||
isSearching
|
isSearching
|
||||||
? _(msg`No search results found for "${search}".`)
|
? _(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},
|
{aspectRatio: 1, opacity: pressed ? 0.8 : 1},
|
||||||
t.atoms.bg_contrast_25,
|
t.atoms.bg_contrast_25,
|
||||||
]}
|
]}
|
||||||
source={{uri: gif.images.preview_gif.url}}
|
source={{
|
||||||
|
uri: gif.media_formats.tinygif.url,
|
||||||
|
}}
|
||||||
contentFit="cover"
|
contentFit="cover"
|
||||||
accessibilityLabel={gif.title}
|
accessibilityLabel={gif.title}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
|
@ -299,61 +293,56 @@ function GifPreview({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function GiphyConsentPrompt({control}: {control: Dialog.DialogControlProps}) {
|
function TenorConsentPrompt({control}: {control: Dialog.DialogControlProps}) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {gtMobile} = useBreakpoints()
|
const {gtMobile} = useBreakpoints()
|
||||||
const setExternalEmbedPref = useSetExternalEmbedPref()
|
const setExternalEmbedPref = useSetExternalEmbedPref()
|
||||||
|
|
||||||
const onShowPress = useCallback(() => {
|
const onShowPress = useCallback(() => {
|
||||||
setExternalEmbedPref('giphy', 'show')
|
setExternalEmbedPref('tenor', 'show')
|
||||||
}, [setExternalEmbedPref])
|
}, [setExternalEmbedPref])
|
||||||
|
|
||||||
const onHidePress = useCallback(() => {
|
const onHidePress = useCallback(() => {
|
||||||
setExternalEmbedPref('giphy', 'hide')
|
setExternalEmbedPref('tenor', 'hide')
|
||||||
control.close()
|
control.close()
|
||||||
}, [control, setExternalEmbedPref])
|
}, [control, setExternalEmbedPref])
|
||||||
|
|
||||||
const gtMobileWeb = gtMobile && isWeb
|
const gtMobileWeb = gtMobile && isWeb
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog.ScrollableInner label={_(msg`Permission to use GIPHY`)}>
|
<Dialog.ScrollableInner label={_(msg`Permission to use Tenor`)}>
|
||||||
<View style={a.gap_sm}>
|
<View style={a.gap_sm}>
|
||||||
<Text style={[a.text_2xl, a.font_bold]}>
|
<Text style={[a.text_2xl, a.font_bold]}>
|
||||||
<Trans>Permission to use GIPHY</Trans>
|
<Trans>Permission to use Tenor</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<View style={[a.mt_sm, a.mb_2xl, a.gap_lg]}>
|
<View style={[a.mt_sm, a.mb_2xl, a.gap_lg]}>
|
||||||
<Text>
|
<Text>
|
||||||
<Trans>
|
<Trans>
|
||||||
Bluesky uses GIPHY to provide the GIF selector feature.
|
Bluesky uses Tenor to provide the GIF selector feature.
|
||||||
</Trans>
|
</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Text style={t.atoms.text_contrast_medium}>
|
<Text style={t.atoms.text_contrast_medium}>
|
||||||
<Trans>
|
<Trans>
|
||||||
GIPHY may collect information about you and your device. You can
|
Tenor is a third-party service that provides GIFs for use in
|
||||||
find out more in their{' '}
|
Bluesky. By enabling Tenor, requests will be made to Tenor's
|
||||||
<InlineLinkText
|
servers to retrieve the GIFs.
|
||||||
to={GIPHY_PRIVACY_POLICY}
|
|
||||||
onPress={() => control.close()}>
|
|
||||||
privacy policy
|
|
||||||
</InlineLinkText>
|
|
||||||
.
|
|
||||||
</Trans>
|
</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View style={[a.gap_md, gtMobileWeb && a.flex_row_reverse]}>
|
<View style={[a.gap_md, gtMobileWeb && a.flex_row_reverse]}>
|
||||||
<Button
|
<Button
|
||||||
label={_(msg`Enable GIPHY`)}
|
label={_(msg`Enable Tenor`)}
|
||||||
onPress={onShowPress}
|
onPress={onShowPress}
|
||||||
onAccessibilityEscape={control.close}
|
onAccessibilityEscape={control.close}
|
||||||
color="primary"
|
color="primary"
|
||||||
size={gtMobileWeb ? 'small' : 'medium'}
|
size={gtMobileWeb ? 'small' : 'medium'}
|
||||||
variant="solid">
|
variant="solid">
|
||||||
<ButtonText>
|
<ButtonText>
|
||||||
<Trans>Enable GIPHY</Trans>
|
<Trans>Enable Tenor</Trans>
|
||||||
</ButtonText>
|
</ButtonText>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -90,11 +90,9 @@ export const BSKY_FEED_OWNER_DIDS = [
|
||||||
'did:plc:q6gjnaw2blty4crticxkmujt',
|
'did:plc:q6gjnaw2blty4crticxkmujt',
|
||||||
]
|
]
|
||||||
|
|
||||||
export const GIPHY_API_URL = 'https://api.giphy.com'
|
export const GIF_SERVICE = 'https://gifs.bsky.app'
|
||||||
export const GIPHY_API_KEY = Platform.select({
|
|
||||||
ios: 'ydVxhrQkwlcUjkVKx15mF6vyaNJbMeez',
|
export const GIF_SEARCH = (params: string) =>
|
||||||
android: 'Vwj3Ib7857dj3EcIg24Hiz1LbRVdGeYF',
|
`${GIF_SERVICE}/tenor/v2/search?${params}`
|
||||||
default: 'vyL3hQQ8AipwcmIB8kFvg0NDs9faWg7G',
|
export const GIF_FEATURED = (params: string) =>
|
||||||
})
|
`${GIF_SERVICE}/tenor/v2/featured?${params}`
|
||||||
export const GIPHY_PRIVACY_POLICY =
|
|
||||||
'https://support.giphy.com/hc/en-us/articles/360032872931-GIPHY-Privacy-Policy'
|
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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'
|
|
@ -29,8 +29,8 @@ import {
|
||||||
useLanguagePrefs,
|
useLanguagePrefs,
|
||||||
useLanguagePrefsApi,
|
useLanguagePrefsApi,
|
||||||
} from '#/state/preferences/languages'
|
} from '#/state/preferences/languages'
|
||||||
import {Gif} from '#/state/queries/giphy'
|
|
||||||
import {useProfileQuery} from '#/state/queries/profile'
|
import {useProfileQuery} from '#/state/queries/profile'
|
||||||
|
import {Gif} from '#/state/queries/tenor'
|
||||||
import {ThreadgateSetting} from '#/state/queries/threadgate'
|
import {ThreadgateSetting} from '#/state/queries/threadgate'
|
||||||
import {getAgent, useSession} from '#/state/session'
|
import {getAgent, useSession} from '#/state/session'
|
||||||
import {useComposerControls} from '#/state/shell/composer'
|
import {useComposerControls} from '#/state/shell/composer'
|
||||||
|
@ -316,18 +316,19 @@ export const ComposePost = observer(function ComposePost({
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const onSelectGif = useCallback(
|
const onSelectGif = useCallback(
|
||||||
(gif: Gif) =>
|
(gif: Gif) => {
|
||||||
setExtLink({
|
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,
|
isLoading: true,
|
||||||
meta: {
|
meta: {
|
||||||
url: gif.url,
|
url: gif.media_formats.gif.url,
|
||||||
image: gif.images.original_still.url,
|
image: gif.media_formats.preview.url,
|
||||||
likelyType: LikelyType.HTML,
|
likelyType: LikelyType.HTML,
|
||||||
title: `${gif.title} - Find & Share on GIPHY`,
|
title: gif.content_description,
|
||||||
description: `ALT: ${gif.alt_text}`,
|
description: `ALT: ${gif.content_description}`,
|
||||||
},
|
},
|
||||||
}),
|
})
|
||||||
|
},
|
||||||
[setExtLink],
|
[setExtLink],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {logEvent} from '#/lib/statsig/statsig'
|
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 {atoms as a, useTheme} from '#/alf'
|
||||||
import {Button} from '#/components/Button'
|
import {Button} from '#/components/Button'
|
||||||
import {useDialogControl} from '#/components/Dialog'
|
import {useDialogControl} from '#/components/Dialog'
|
||||||
|
|
Loading…
Reference in New Issue