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

Configure Feed

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

at theme-changes 187 lines 4.6 kB view raw
1import { 2 type AppBskyEmbedRecord, 3 type AppBskyFeedDefs, 4 AppBskyFeedPost, 5 AtUri, 6 type BskyAgent, 7} from '@atproto/api' 8import {type ProfileViewBasic} from '@atproto/api/dist/client/types/app/bsky/actor/defs' 9import {useQuery} from '@tanstack/react-query' 10 11import {retry} from '#/lib/async/retry' 12import {STALE} from '#/state/queries' 13import {useAgent} from '#/state/session' 14import * as bsky from '#/types/bsky' 15import {type EmbedType} from '#/types/bsky/post' 16 17const RQKEY_ROOT = 'direct-fetch-record' 18export const RQKEY = (uri: string) => [RQKEY_ROOT, uri] 19 20export async function directFetchRecordAndProfile( 21 agent: BskyAgent, 22 uri: string, 23) { 24 const urip = new AtUri(uri) 25 26 if (!urip.host.startsWith('did:')) { 27 const res = await agent.resolveHandle({ 28 handle: urip.host, 29 }) 30 urip.host = res.data.did as `did:${string}:${string}` 31 } 32 33 try { 34 const [profile, record] = await Promise.all([ 35 (async () => (await agent.getProfile({actor: urip.host})).data)(), 36 (async () => 37 ( 38 await retry( 39 2, 40 e => { 41 if (e.message.includes(`Could not locate record:`)) { 42 return false 43 } 44 return true 45 }, 46 () => 47 agent.api.com.atproto.repo.getRecord({ 48 repo: urip.host, 49 collection: 'app.bsky.feed.post', 50 rkey: urip.rkey, 51 }), 52 ) 53 ).data.value)(), 54 ]) 55 56 return {profile, record} 57 } catch (e) { 58 console.error(e) 59 return undefined 60 } 61} 62 63export async function directFetchEmbedRecord( 64 agent: BskyAgent, 65 uri: string, 66): Promise<EmbedType<'post'> | undefined> { 67 const res = await directFetchRecordAndProfile(agent, uri) 68 if (res === undefined) return undefined 69 const {profile, record} = res 70 71 if (record && bsky.validate(record, AppBskyFeedPost.validateRecord)) { 72 return { 73 type: 'post', 74 view: { 75 $type: 'app.bsky.embed.record#viewRecord', 76 uri, 77 author: profile as ProfileViewBasic, 78 cid: 'directfetch', 79 value: record, 80 indexedAt: new Date().toISOString(), 81 } satisfies AppBskyEmbedRecord.ViewRecord, 82 } 83 } else { 84 return undefined 85 } 86} 87 88export function useDirectFetchEmbedRecord({ 89 uri, 90 enabled, 91}: { 92 uri: string 93 enabled?: boolean 94}) { 95 const agent = useAgent() 96 return useQuery<EmbedType<'post'> | undefined>({ 97 staleTime: STALE.HOURS.ONE, 98 queryKey: RQKEY(uri || ''), 99 async queryFn() { 100 return directFetchEmbedRecord(agent, uri) 101 }, 102 enabled: enabled && !!uri, 103 }) 104} 105 106export async function directFetchPostRecord( 107 agent: BskyAgent, 108 uri: string, 109): Promise<AppBskyFeedDefs.PostView | undefined> { 110 const res = await directFetchRecordAndProfile(agent, uri) 111 if (res === undefined) return undefined 112 const {profile, record} = res 113 114 if (record && bsky.validate(record, AppBskyFeedPost.validateRecord)) { 115 return { 116 $type: 'app.bsky.feed.defs#postView', 117 uri, 118 author: profile as ProfileViewBasic, 119 cid: 'directfetch', 120 record, 121 indexedAt: new Date().toISOString(), 122 } satisfies AppBskyFeedDefs.PostView 123 } else { 124 return undefined 125 } 126} 127 128// based on https://stackoverflow.com/a/46432113 129export class LRU<K, V> { 130 max: number 131 private cache: Map<K, Promise<V>> 132 constructor(max = 1_024) { 133 this.max = max 134 this.cache = new Map() 135 } 136 137 get(key: K) { 138 let item = this.cache.get(key) 139 if (item !== undefined) { 140 // refresh key 141 this.cache.delete(key) 142 this.cache.set(key, item) 143 } 144 return item 145 } 146 147 set(key: K, val: Promise<V>) { 148 // refresh key 149 if (this.cache.has(key)) this.cache.delete(key) 150 // evict oldest 151 else if (this.cache.size >= this.max) 152 this.cache.delete(this.nonemptyFirst()) 153 this.cache.set(key, val) 154 } 155 156 delete(key: K) { 157 return this.cache.delete(key) 158 } 159 160 private nonemptyFirst() { 161 return this.cache.keys().next().value! 162 } 163 164 async getOrInsertWith(key: K, fn: () => Promise<V>): Promise<V> { 165 const val = this.get(key) 166 if (val !== undefined) return val 167 168 const promise = fn() 169 this.set(key, promise) 170 return promise 171 } 172 173 // try to insert, but remove from cache on error and bubble 174 async getOrTryInsertWith(key: K, fn: () => Promise<V>): Promise<V> { 175 const val = this.get(key) 176 if (val !== undefined) return val 177 178 const promise = fn() 179 this.set(key, promise) 180 try { 181 return await promise 182 } catch (e) { 183 this.delete(key) 184 throw e 185 } 186 } 187}