This commit is contained in:
Eric Bailey
2024-09-05 16:42:02 -05:00
parent ae3f2015f2
commit 06878b0fcb
8 changed files with 264 additions and 92 deletions
+37 -26
View File
@@ -29,6 +29,11 @@ import {Provider as A11yProvider} from '#/state/a11y'
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes' import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
import {Provider as DialogStateProvider} from '#/state/dialogs' import {Provider as DialogStateProvider} from '#/state/dialogs'
import {listenSessionDropped} from '#/state/events' import {listenSessionDropped} from '#/state/events'
import {
beginResolveGeolocation,
ensureGeolocationResolved,
Provider as GeolocationProvider,
} from '#/state/geolocation'
import {Provider as InvitesStateProvider} from '#/state/invites' import {Provider as InvitesStateProvider} from '#/state/invites'
import {Provider as LightboxStateProvider} from '#/state/lightbox' import {Provider as LightboxStateProvider} from '#/state/lightbox'
import {MessagesProvider} from '#/state/messages' import {MessagesProvider} from '#/state/messages'
@@ -85,6 +90,7 @@ function InnerApp() {
await initialize() await initialize()
await tryFetchGates(undefined, 'prefer-fresh-gates') await tryFetchGates(undefined, 'prefer-fresh-gates')
} }
await ensureGeolocationResolved()
} catch (e) { } catch (e) {
logger.error(`session: resume failed`, {message: e}) logger.error(`session: resume failed`, {message: e})
} finally { } finally {
@@ -158,9 +164,12 @@ function App() {
const [isReady, setReady] = useState(false) const [isReady, setReady] = useState(false)
React.useEffect(() => { React.useEffect(() => {
beginResolveGeolocation()
PlatformInfo.setAudioCategory(AudioCategory.Ambient) PlatformInfo.setAudioCategory(AudioCategory.Ambient)
PlatformInfo.setAudioActive(false) PlatformInfo.setAudioActive(false)
initPersistedState().then(() => setReady(true)) Promise.all([initPersistedState(), ensureGeolocationResolved()]).then(() =>
setReady(true),
)
}, []) }, [])
if (!isReady) { if (!isReady) {
@@ -172,31 +181,33 @@ function App() {
* that is set up in the InnerApp component above. * that is set up in the InnerApp component above.
*/ */
return ( return (
<A11yProvider> <GeolocationProvider>
<KeyboardProvider enabled={false} statusBarTranslucent={true}> <A11yProvider>
<SessionProvider> <KeyboardProvider enabled={false} statusBarTranslucent={true}>
<PrefsStateProvider> <SessionProvider>
<I18nProvider> <PrefsStateProvider>
<ShellStateProvider> <I18nProvider>
<InvitesStateProvider> <ShellStateProvider>
<ModalStateProvider> <InvitesStateProvider>
<DialogStateProvider> <ModalStateProvider>
<LightboxStateProvider> <DialogStateProvider>
<PortalProvider> <LightboxStateProvider>
<StarterPackProvider> <PortalProvider>
<InnerApp /> <StarterPackProvider>
</StarterPackProvider> <InnerApp />
</PortalProvider> </StarterPackProvider>
</LightboxStateProvider> </PortalProvider>
</DialogStateProvider> </LightboxStateProvider>
</ModalStateProvider> </DialogStateProvider>
</InvitesStateProvider> </ModalStateProvider>
</ShellStateProvider> </InvitesStateProvider>
</I18nProvider> </ShellStateProvider>
</PrefsStateProvider> </I18nProvider>
</SessionProvider> </PrefsStateProvider>
</KeyboardProvider> </SessionProvider>
</A11yProvider> </KeyboardProvider>
</A11yProvider>
</GeolocationProvider>
) )
} }
+35 -24
View File
@@ -18,6 +18,11 @@ import {Provider as A11yProvider} from '#/state/a11y'
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes' import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
import {Provider as DialogStateProvider} from '#/state/dialogs' import {Provider as DialogStateProvider} from '#/state/dialogs'
import {listenSessionDropped} from '#/state/events' import {listenSessionDropped} from '#/state/events'
import {
beginResolveGeolocation,
ensureGeolocationResolved,
Provider as GeolocationProvider,
} from '#/state/geolocation'
import {Provider as InvitesStateProvider} from '#/state/invites' import {Provider as InvitesStateProvider} from '#/state/invites'
import {Provider as LightboxStateProvider} from '#/state/lightbox' import {Provider as LightboxStateProvider} from '#/state/lightbox'
import {MessagesProvider} from '#/state/messages' import {MessagesProvider} from '#/state/messages'
@@ -71,6 +76,7 @@ function InnerApp() {
} finally { } finally {
setIsReady(true) setIsReady(true)
} }
await ensureGeolocationResolved()
} }
const account = readLastActiveAccount() const account = readLastActiveAccount()
onLaunch(account) onLaunch(account)
@@ -139,7 +145,10 @@ function App() {
const [isReady, setReady] = useState(false) const [isReady, setReady] = useState(false)
React.useEffect(() => { React.useEffect(() => {
initPersistedState().then(() => setReady(true)) beginResolveGeolocation()
Promise.all([initPersistedState(), ensureGeolocationResolved()]).then(() =>
setReady(true),
)
}, []) }, [])
if (!isReady) { if (!isReady) {
@@ -151,29 +160,31 @@ function App() {
* that is set up in the InnerApp component above. * that is set up in the InnerApp component above.
*/ */
return ( return (
<A11yProvider> <GeolocationProvider>
<SessionProvider> <A11yProvider>
<PrefsStateProvider> <SessionProvider>
<I18nProvider> <PrefsStateProvider>
<ShellStateProvider> <I18nProvider>
<InvitesStateProvider> <ShellStateProvider>
<ModalStateProvider> <InvitesStateProvider>
<DialogStateProvider> <ModalStateProvider>
<LightboxStateProvider> <DialogStateProvider>
<PortalProvider> <LightboxStateProvider>
<StarterPackProvider> <PortalProvider>
<InnerApp /> <StarterPackProvider>
</StarterPackProvider> <InnerApp />
</PortalProvider> </StarterPackProvider>
</LightboxStateProvider> </PortalProvider>
</DialogStateProvider> </LightboxStateProvider>
</ModalStateProvider> </DialogStateProvider>
</InvitesStateProvider> </ModalStateProvider>
</ShellStateProvider> </InvitesStateProvider>
</I18nProvider> </ShellStateProvider>
</PrefsStateProvider> </I18nProvider>
</SessionProvider> </PrefsStateProvider>
</A11yProvider> </SessionProvider>
</A11yProvider>
</GeolocationProvider>
) )
} }
-25
View File
@@ -1,25 +0,0 @@
import {emitGeoUpdated} from '#/state/events'
let geo: any
export async function resolveGeo() {
const req = new Promise(y => {
setTimeout(() => {
try {
geo = {country_code: 'US'}
emitGeoUpdated({geo})
} catch (e) {
} finally {
y(0)
}
}, 1000)
})
if (!geo) {
await req
}
}
export function getGeo() {
return geo
}
-8
View File
@@ -45,11 +45,3 @@ export function listenPostCreated(fn: () => void): UnlistenFn {
emitter.on('post-created', fn) emitter.on('post-created', fn)
return () => emitter.off('post-created', fn) return () => emitter.off('post-created', fn)
} }
export function emitGeoUpdated({geo}: {geo: any}) {
emitter.emit('geo-updated', geo)
}
export function listenGeoUpdated(fn: ({geo}: {geo: any}) => void): UnlistenFn {
emitter.on('geo-updated', fn)
return () => emitter.off('geo-updated', fn)
}
+156
View File
@@ -0,0 +1,156 @@
import React from 'react'
import EventEmitter from 'eventemitter3'
import {networkRetry} from '#/lib/async/retry'
import {logger} from '#/logger'
import {Device, device} from '#/storage'
const events = new EventEmitter()
const EVENT = 'geolocation-updated'
const emitGeolocationUpdate = (geolocation: Device['geolocation']) => {
events.emit(EVENT, geolocation)
}
const onGeolocationUpdate = (
listener: (geolocation: Device['geolocation']) => void,
) => {
events.on(EVENT, listener)
return () => {
events.off(EVENT, listener)
}
}
/**
* Default geolocation value. IF undefined, we fail closed and apply all
* additional mod authorities.
*/
export const DEFAULT_GEOLOCATION: Device['geolocation'] = {
countryCode: undefined,
}
async function getGeolocation(): Promise<Device['geolocation']> {
const res = await fetch(`https://api.bsky.app/xrpc/_health`)
if (!res.ok) {
throw new Error(`geolocation: lookup failed ${res.status}`)
}
const json = {country_code: 'US'} // await res.json()
if (json.country_code) {
return {
countryCode: json.country_code,
}
} else {
return undefined
}
}
/**
* Local promise used within this file only.
*/
let geolocationResolution: Promise<void> | undefined
/**
* Begin the process of resolving geolocation. This should be called once at
* app start.
*
* THIS METHOD SHOULD NEVER THROW.
*
* This method is otherwise not used for any purpose. To ensure geolocation is
* resolved, use {@link ensureGeolocationResolved}
*/
export function beginResolveGeolocation() {
geolocationResolution = new Promise(async resolve => {
try {
// Try once, fail fast
const geolocation = await getGeolocation()
if (geolocation) {
device.set(['geolocation'], geolocation)
emitGeolocationUpdate(geolocation)
} else {
// endpoint should throw on all failures, this is insurance
throw new Error(`geolocation: nothing returned from initial request`)
}
} catch (e: any) {
logger.error(`geolocation: failed initial request`, {
safeMessage: e.message,
})
// set to default
device.set(['geolocation'], DEFAULT_GEOLOCATION)
// retry 3 times, but don't await, proceed with default
networkRetry(3, getGeolocation)
.then(geolocation => {
if (geolocation) {
device.set(['geolocation'], geolocation)
emitGeolocationUpdate(geolocation)
} else {
// endpoint should throw on all failures, this is insurance
throw new Error(`geolocation: nothing returned from retries`)
}
})
.catch((e: any) => {
// complete fail closed
logger.error(`geolocation: failed retries`, {safeMessage: e.message})
})
} finally {
resolve(undefined)
}
})
}
/**
* Ensure that geolocation has been resolved, or at the very least attempted
* once. Subsequent retries will not be captured by this `await`. Those will be
* reported via {@link events}.
*/
export async function ensureGeolocationResolved() {
if (!geolocationResolution) {
throw new Error(`geolocation: beginResolveGeolocation not called yet`)
}
const cached = device.get(['geolocation'])
if (cached) {
logger.info(`geolocation: using cache`, {cached})
} else {
logger.info(`geolocation: no cache`)
await geolocationResolution
logger.info(`geolocation: resolved`, {
resolved: device.get(['geolocation']),
})
}
}
type Context = {
geolocation: Device['geolocation']
}
const context = React.createContext<Context>({
geolocation: DEFAULT_GEOLOCATION,
})
export function Provider({children}: {children: React.ReactNode}) {
const [geolocation, setGeolocation] = React.useState(() => {
const initial = device.get(['geolocation']) || DEFAULT_GEOLOCATION
return initial
})
React.useEffect(() => {
return onGeolocationUpdate(geolocation => {
setGeolocation(geolocation!)
})
}, [])
const ctx = React.useMemo(() => {
return {
geolocation,
}
}, [geolocation])
return <context.Provider value={ctx}>{children}</context.Provider>
}
export function useGeolocation() {
return React.useContext(context)
}
@@ -1,9 +1,17 @@
import {BskyAgent} from '@atproto/api' import {BskyAgent} from '@atproto/api'
import {logger} from '#/logger'
import {device} from '#/storage'
export const ADDITIONAL_LABELER = 'did:plc:oz5zavafp7szpd2yyko57ccz' export const ADDITIONAL_LABELER = 'did:plc:oz5zavafp7szpd2yyko57ccz'
export const ADDITIONAL_LABELERS_MAP = { export const ADDITIONAL_LABELERS_MAP: {
category: [ADDITIONAL_LABELER], [countryCode: string]: string[]
} = {
US: [ADDITIONAL_LABELER],
} }
export const ALL_ADDITIONAL_LABELERS = Object.values(
ADDITIONAL_LABELERS_MAP,
).flat()
export const NON_CONFIGURABLE_LABELERS = [ADDITIONAL_LABELER] export const NON_CONFIGURABLE_LABELERS = [ADDITIONAL_LABELER]
export function isNonConfigurableModerationAuthority(did: string) { export function isNonConfigurableModerationAuthority(did: string) {
@@ -11,10 +19,23 @@ export function isNonConfigurableModerationAuthority(did: string) {
} }
export function configureAdditionalModerationAuthorities() { export function configureAdditionalModerationAuthorities() {
BskyAgent.configure({ const geolocation = device.get(['geolocation'])
appLabelers: [ let additionalLabelers: string[] = ALL_ADDITIONAL_LABELERS
...BskyAgent.appLabelers,
...(ADDITIONAL_LABELERS_MAP.category ?? []), if (geolocation?.countryCode) {
], additionalLabelers = ADDITIONAL_LABELERS_MAP[geolocation.countryCode] ?? []
} else {
logger.info(`no geolocation, cannot apply mod authorities`)
}
const appLabelers = Array.from(
new Set([...BskyAgent.appLabelers, ...additionalLabelers]),
)
logger.debug(`applying mod authorities`, {
additionalLabelers,
appLabelers,
}) })
BskyAgent.configure({appLabelers})
} }
+3 -1
View File
@@ -2,6 +2,8 @@ import {MMKV} from 'react-native-mmkv'
import {Device} from '#/storage/schema' import {Device} from '#/storage/schema'
export * from '#/storage/schema'
/** /**
* Generic storage class. DO NOT use this directly. Instead, use the exported * Generic storage class. DO NOT use this directly. Instead, use the exported
* storage instances below. * storage instances below.
@@ -69,4 +71,4 @@ export class Storage<Scopes extends unknown[], Schema> {
* *
* `device.set([key], true)` * `device.set([key], true)`
*/ */
export const device = new Storage<[], Device>({id: 'device'}) export const device = new Storage<[], Device>({id: 'bsky_device'})
+5 -1
View File
@@ -1,4 +1,8 @@
/** /**
* Device data that's specific to the device and does not vary based account * Device data that's specific to the device and does not vary based account
*/ */
export type Device = {} export type Device = {
geolocation?: {
countryCode: string | undefined
}
}