forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
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}