[GIFs] Replace GIPHY with Tenor (#3651)
* replace GIPHY with Tenor * remove "directly" wording * replace GIPHY wording * remove log
This commit is contained in:
parent
1a4e05e9f9
commit
76449fb6ef
6 changed files with 220 additions and 335 deletions
|
@ -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
177
src/state/queries/tenor.ts
Normal 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'
|
Loading…
Add table
Add a link
Reference in a new issue