about summary refs log tree commit diff
path: root/src/state/queries
diff options
context:
space:
mode:
Diffstat (limited to 'src/state/queries')
-rw-r--r--src/state/queries/giphy.ts280
-rw-r--r--src/state/queries/tenor.ts177
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'