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