forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
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 {isNative, isWeb} from '#/platform/detection'
13import {listenNetworkConfirmed, listenNetworkLost} from '#/state/events'
14
15declare global {
16 interface Window {
17 __TANSTACK_QUERY_CLIENT__: import('@tanstack/query-core').QueryClient
18 }
19}
20
21// any query keys in this array will be persisted to AsyncStorage
22export const labelersDetailedInfoQueryKeyRoot = 'labelers-detailed-info'
23const STORED_CACHE_QUERY_KEY_ROOTS = [labelersDetailedInfoQueryKeyRoot]
24
25async function checkIsOnline(): Promise<boolean> {
26 try {
27 const controller = new AbortController()
28 setTimeout(() => {
29 controller.abort()
30 }, 15e3)
31 const res = await fetch('https://public.api.bsky.app/xrpc/_health', {
32 cache: 'no-store',
33 signal: controller.signal,
34 })
35 const json = await res.json()
36 if (json.version) {
37 return true
38 } else {
39 return false
40 }
41 } catch (e) {
42 return false
43 }
44}
45
46let receivedNetworkLost = false
47let receivedNetworkConfirmed = false
48let isNetworkStateUnclear = false
49
50listenNetworkLost(() => {
51 receivedNetworkLost = true
52 onlineManager.setOnline(false)
53})
54
55listenNetworkConfirmed(() => {
56 receivedNetworkConfirmed = true
57 onlineManager.setOnline(true)
58})
59
60let checkPromise: Promise<void> | undefined
61function checkIsOnlineIfNeeded() {
62 if (checkPromise) {
63 return
64 }
65 receivedNetworkLost = false
66 receivedNetworkConfirmed = false
67 checkPromise = checkIsOnline().then(nextIsOnline => {
68 checkPromise = undefined
69 if (nextIsOnline && receivedNetworkLost) {
70 isNetworkStateUnclear = true
71 }
72 if (!nextIsOnline && receivedNetworkConfirmed) {
73 isNetworkStateUnclear = true
74 }
75 if (!isNetworkStateUnclear) {
76 onlineManager.setOnline(nextIsOnline)
77 }
78 })
79}
80
81setInterval(() => {
82 if (AppState.currentState === 'active') {
83 if (!onlineManager.isOnline() || isNetworkStateUnclear) {
84 checkIsOnlineIfNeeded()
85 }
86 }
87}, 2000)
88
89focusManager.setEventListener(onFocus => {
90 if (isNative) {
91 const subscription = AppState.addEventListener(
92 'change',
93 (status: AppStateStatus) => {
94 focusManager.setFocused(status === 'active')
95 },
96 )
97
98 return () => subscription.remove()
99 } else if (typeof window !== 'undefined' && window.addEventListener) {
100 // these handlers are a bit redundant but focus catches when the browser window
101 // is blurred/focused while visibilitychange seems to only handle when the
102 // window minimizes (both of them catch tab changes)
103 // there's no harm to redundant fires because refetchOnWindowFocus is only
104 // used with queries that employ stale data times
105 const handler = () => onFocus()
106 window.addEventListener('focus', handler, false)
107 window.addEventListener('visibilitychange', handler, false)
108 return () => {
109 window.removeEventListener('visibilitychange', handler)
110 window.removeEventListener('focus', handler)
111 }
112 }
113})
114
115const createQueryClient = () =>
116 new QueryClient({
117 defaultOptions: {
118 queries: {
119 // NOTE
120 // refetchOnWindowFocus breaks some UIs (like feeds)
121 // so we only selectively want to enable this
122 // -prf
123 refetchOnWindowFocus: false,
124 // Structural sharing between responses makes it impossible to rely on
125 // "first seen" timestamps on objects to determine if they're fresh.
126 // Disable this optimization so that we can rely on "first seen" timestamps.
127 structuralSharing: false,
128 // We don't want to retry queries by default, because in most cases we
129 // want to fail early and show a response to the user. There are
130 // exceptions, and those can be made on a per-query basis. For others, we
131 // should give users controls to retry.
132 retry: false,
133 },
134 },
135 })
136
137const dehydrateOptions: PersistQueryClientProviderProps['persistOptions']['dehydrateOptions'] =
138 {
139 shouldDehydrateMutation: (_: any) => false,
140 shouldDehydrateQuery: query => {
141 return STORED_CACHE_QUERY_KEY_ROOTS.includes(String(query.queryKey[0]))
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 asyncPersister = createAsyncStoragePersister({
181 storage: AsyncStorage,
182 key: 'queryClient-' + (currentDid ?? 'logged-out'),
183 })
184 return {
185 persister: asyncPersister,
186 dehydrateOptions,
187 }
188 })
189 useEffect(() => {
190 if (isWeb) {
191 window.__TANSTACK_QUERY_CLIENT__ = queryClient
192 }
193 }, [queryClient])
194 return (
195 <PersistQueryClientProvider
196 client={queryClient}
197 persistOptions={persistOptions}>
198 {children}
199 </PersistQueryClientProvider>
200 )
201}