diff options
Diffstat (limited to 'src/state/queries')
-rw-r--r-- | src/state/queries/giphy.ts | 280 | ||||
-rw-r--r-- | src/state/queries/tenor.ts | 177 |
2 files changed, 177 insertions, 280 deletions
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' |