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 {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}