[GIFs] Replace GIPHY with Tenor (#3651)

* replace GIPHY with Tenor

* remove "directly" wording

* replace GIPHY wording

* remove log
This commit is contained in:
Samuel Newman 2024-04-22 23:39:32 +01:00 committed by GitHub
parent 1a4e05e9f9
commit 76449fb6ef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 220 additions and 335 deletions

View file

@ -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
}

177
src/state/queries/tenor.ts Normal file
View file

@ -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'