Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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