Bluesky app fork with some witchin' additions 馃挮 witchsky.app
bluesky fork client
at main 203 lines 6.2 kB view raw
1import {useEffect, useRef, useState} from 'react' 2import {AppState, type AppStateStatus} from 'react-native' 3import {createAsyncStoragePersister} from '@tanstack/query-async-storage-persister' 4import {focusManager, onlineManager, QueryClient} from '@tanstack/react-query' 5import { 6 type PersistQueryClientOptions, 7 PersistQueryClientProvider, 8 type PersistQueryClientProviderProps, 9} from '@tanstack/react-query-persist-client' 10 11import {createPersistedQueryStorage} from '#/lib/persisted-query-storage' 12import {listenNetworkConfirmed, listenNetworkLost} from '#/state/events' 13import {PERSISTED_QUERY_ROOT} from '#/state/queries' 14import * as env from '#/env' 15import {IS_NATIVE, IS_WEB} from '#/env' 16 17declare global { 18 interface Window { 19 // eslint-disable-next-line @typescript-eslint/consistent-type-imports 20 __TANSTACK_QUERY_CLIENT__: import('@tanstack/query-core').QueryClient 21 } 22} 23 24async function checkIsOnline(): Promise<boolean> { 25 try { 26 const controller = new AbortController() 27 setTimeout(() => { 28 controller.abort() 29 }, 15e3) 30 const res = await fetch('https://public.api.bsky.app/xrpc/_health', { 31 cache: 'no-store', 32 signal: controller.signal, 33 }) 34 const json = await res.json() 35 if (json.version) { 36 return true 37 } else { 38 return false 39 } 40 } catch (e) { 41 return false 42 } 43} 44 45let receivedNetworkLost = false 46let receivedNetworkConfirmed = false 47let isNetworkStateUnclear = false 48 49listenNetworkLost(() => { 50 receivedNetworkLost = true 51 onlineManager.setOnline(false) 52}) 53 54listenNetworkConfirmed(() => { 55 receivedNetworkConfirmed = true 56 onlineManager.setOnline(true) 57}) 58 59let checkPromise: Promise<void> | undefined 60function checkIsOnlineIfNeeded() { 61 if (checkPromise) { 62 return 63 } 64 receivedNetworkLost = false 65 receivedNetworkConfirmed = false 66 checkPromise = checkIsOnline().then(nextIsOnline => { 67 checkPromise = undefined 68 if (nextIsOnline && receivedNetworkLost) { 69 isNetworkStateUnclear = true 70 } 71 if (!nextIsOnline && receivedNetworkConfirmed) { 72 isNetworkStateUnclear = true 73 } 74 if (!isNetworkStateUnclear) { 75 onlineManager.setOnline(nextIsOnline) 76 } 77 }) 78} 79 80setInterval(() => { 81 if (AppState.currentState === 'active') { 82 if (!onlineManager.isOnline() || isNetworkStateUnclear) { 83 checkIsOnlineIfNeeded() 84 } 85 } 86}, 2000) 87 88focusManager.setEventListener(onFocus => { 89 if (IS_NATIVE) { 90 const subscription = AppState.addEventListener( 91 'change', 92 (status: AppStateStatus) => { 93 focusManager.setFocused(status === 'active') 94 }, 95 ) 96 97 return () => subscription.remove() 98 } else if (typeof window !== 'undefined' && window.addEventListener) { 99 // these handlers are a bit redundant but focus catches when the browser window 100 // is blurred/focused while visibilitychange seems to only handle when the 101 // window minimizes (both of them catch tab changes) 102 // there's no harm to redundant fires because refetchOnWindowFocus is only 103 // used with queries that employ stale data times 104 const handler = () => onFocus() 105 window.addEventListener('focus', handler, false) 106 window.addEventListener('visibilitychange', handler, false) 107 return () => { 108 window.removeEventListener('visibilitychange', handler) 109 window.removeEventListener('focus', handler) 110 } 111 } 112}) 113 114const createQueryClient = () => 115 new QueryClient({ 116 defaultOptions: { 117 queries: { 118 // NOTE 119 // refetchOnWindowFocus breaks some UIs (like feeds) 120 // so we only selectively want to enable this 121 // -prf 122 refetchOnWindowFocus: false, 123 // Structural sharing between responses makes it impossible to rely on 124 // "first seen" timestamps on objects to determine if they're fresh. 125 // Disable this optimization so that we can rely on "first seen" timestamps. 126 structuralSharing: false, 127 // We don't want to retry queries by default, because in most cases we 128 // want to fail early and show a response to the user. There are 129 // exceptions, and those can be made on a per-query basis. For others, we 130 // should give users controls to retry. 131 retry: false, 132 }, 133 }, 134 }) 135 136const dehydrateOptions: PersistQueryClientProviderProps['persistOptions']['dehydrateOptions'] = 137 { 138 shouldDehydrateMutation: (_: any) => false, 139 shouldDehydrateQuery: query => { 140 const root = String(query.queryKey[0]) 141 return root === PERSISTED_QUERY_ROOT 142 }, 143 } 144 145export function QueryProvider({ 146 children, 147 currentDid, 148}: { 149 children: React.ReactNode 150 currentDid: string | undefined 151}) { 152 return ( 153 <QueryProviderInner 154 // Enforce we never reuse cache between users. 155 // These two props MUST stay in sync. 156 key={currentDid} 157 currentDid={currentDid}> 158 {children} 159 </QueryProviderInner> 160 ) 161} 162 163function QueryProviderInner({ 164 children, 165 currentDid, 166}: { 167 children: React.ReactNode 168 currentDid: string | undefined 169}) { 170 const initialDid = useRef(currentDid) 171 if (currentDid !== initialDid.current) { 172 throw Error( 173 'Something is very wrong. Expected did to be stable due to key above.', 174 ) 175 } 176 // We create the query client here so that it's scoped to a specific DID. 177 // Do not move the query client creation outside of this component. 178 const [queryClient, _setQueryClient] = useState(() => createQueryClient()) 179 const [persistOptions, _setPersistOptions] = useState(() => { 180 const storage = createPersistedQueryStorage(currentDid ?? 'logged-out') 181 const asyncPersister = createAsyncStoragePersister({ 182 storage, 183 key: 'queryClient-' + (currentDid ?? 'logged-out'), 184 }) 185 return { 186 persister: asyncPersister, 187 dehydrateOptions, 188 buster: env.APP_VERSION, 189 } satisfies Omit<PersistQueryClientOptions, 'queryClient'> 190 }) 191 useEffect(() => { 192 if (IS_WEB) { 193 window.__TANSTACK_QUERY_CLIENT__ = queryClient 194 } 195 }, [queryClient]) 196 return ( 197 <PersistQueryClientProvider 198 client={queryClient} 199 persistOptions={persistOptions}> 200 {children} 201 </PersistQueryClientProvider> 202 ) 203}