mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import {Platform} from 'react-native'
2import {getLocales} from 'expo-localization'
3import {keepPreviousData, useInfiniteQuery} from '@tanstack/react-query'
4
5import {GIF_FEATURED, GIF_SEARCH} from '#/lib/constants'
6
7export const RQKEY_ROOT = 'gif-service'
8export const RQKEY_FEATURED = [RQKEY_ROOT, 'featured']
9export const RQKEY_SEARCH = (query: string) => [RQKEY_ROOT, 'search', query]
10
11const getTrendingGifs = createTenorApi(GIF_FEATURED)
12
13const searchGifs = createTenorApi<{q: string}>(GIF_SEARCH)
14
15export function useFeaturedGifsQuery() {
16 return useInfiniteQuery({
17 queryKey: RQKEY_FEATURED,
18 queryFn: ({pageParam}) => getTrendingGifs({pos: pageParam}),
19 initialPageParam: undefined as string | undefined,
20 getNextPageParam: lastPage => lastPage.next,
21 })
22}
23
24export function useGifSearchQuery(query: string) {
25 return useInfiniteQuery({
26 queryKey: RQKEY_SEARCH(query),
27 queryFn: ({pageParam}) => searchGifs({q: query, pos: pageParam}),
28 initialPageParam: undefined as string | undefined,
29 getNextPageParam: lastPage => lastPage.next,
30 enabled: !!query,
31 placeholderData: keepPreviousData,
32 })
33}
34
35function createTenorApi<Input extends object>(
36 urlFn: (params: string) => string,
37): (input: Input & {pos?: string}) => Promise<{
38 next: string
39 results: Gif[]
40}> {
41 return async input => {
42 const params = new URLSearchParams()
43
44 // set client key based on platform
45 params.set(
46 'client_key',
47 Platform.select({
48 ios: 'bluesky-ios',
49 android: 'bluesky-android',
50 default: 'bluesky-web',
51 }),
52 )
53
54 // 30 is divisible by 2 and 3, so both 2 and 3 column layouts can be used
55 params.set('limit', '30')
56
57 params.set('contentfilter', 'high')
58
59 params.set(
60 'media_filter',
61 (['preview', 'gif', 'tinygif'] satisfies ContentFormats[]).join(','),
62 )
63
64 const locale = getLocales?.()?.[0]
65
66 if (locale) {
67 params.set('locale', locale.languageTag.replace('-', '_'))
68 }
69
70 for (const [key, value] of Object.entries(input)) {
71 if (value !== undefined) {
72 params.set(key, String(value))
73 }
74 }
75
76 const res = await fetch(urlFn(params.toString()), {
77 method: 'GET',
78 headers: {
79 'Content-Type': 'application/json',
80 },
81 })
82 if (!res.ok) {
83 throw new Error('Failed to fetch Tenor API')
84 }
85 return res.json()
86 }
87}
88
89export type Gif = {
90 /**
91 * A Unix timestamp that represents when this post was created.
92 */
93 created: number
94 /**
95 * Returns true if this post contains audio.
96 * Note: Only video formats support audio. The GIF image file format can't contain audio information.
97 */
98 hasaudio: boolean
99 /**
100 * Tenor result identifier
101 */
102 id: string
103 /**
104 * A dictionary with a content format as the key and a Media Object as the value.
105 */
106 media_formats: Record<ContentFormats, MediaObject>
107 /**
108 * An array of tags for the post
109 */
110 tags: string[]
111 /**
112 * The title of the post
113 */
114 title: string
115 /**
116 * A textual description of the content.
117 * We recommend that you use content_description for user accessibility features.
118 */
119 content_description: string
120 /**
121 * The full URL to view the post on tenor.com.
122 */
123 itemurl: string
124 /**
125 * Returns true if this post contains captions.
126 */
127 hascaption: boolean
128 /**
129 * 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.
130 */
131 flags: string
132 /**
133 * The most common background pixel color of the content
134 */
135 bg_color?: string
136 /**
137 * A short URL to view the post on tenor.com.
138 */
139 url: string
140}
141
142type MediaObject = {
143 /**
144 * A URL to the media source
145 */
146 url: string
147 /**
148 * Width and height of the media in pixels
149 */
150 dims: [number, number]
151 /**
152 * Represents the time in seconds for one loop of the content. If the content is static, the duration is set to 0.
153 */
154 duration: number
155 /**
156 * Size of the file in bytes
157 */
158 size: number
159}
160
161type ContentFormats =
162 | 'preview'
163 | 'gif'
164 // | 'mediumgif'
165 | 'tinygif'
166// | 'nanogif'
167// | 'mp4'
168// | 'loopedmp4'
169// | 'tinymp4'
170// | 'nanomp4'
171// | 'webm'
172// | 'tinywebm'
173// | 'nanowebm'