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