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 DialogStateProvider} from '#/state/dialogs'
import {listenSessionDropped} from '#/state/events'
import {
beginResolveGeolocation,
ensureGeolocationResolved,
Provider as GeolocationProvider,
} from '#/state/geolocation'
import {Provider as InvitesStateProvider} from '#/state/invites'
import {Provider as LightboxStateProvider} from '#/state/lightbox'
import {MessagesProvider} from '#/state/messages'
@@ -85,6 +90,7 @@ function InnerApp() {
await initialize()
await tryFetchGates(undefined, 'prefer-fresh-gates')
}
await ensureGeolocationResolved()
} catch (e) {
logger.error(`session: resume failed`, {message: e})
} finally {
@@ -158,9 +164,12 @@ function App() {
const [isReady, setReady] = useState(false)
React.useEffect(() => {
beginResolveGeolocation()
PlatformInfo.setAudioCategory(AudioCategory.Ambient)
PlatformInfo.setAudioActive(false)
initPersistedState().then(() => setReady(true))
Promise.all([initPersistedState(), ensureGeolocationResolved()]).then(() =>
setReady(true),
)
}, [])
if (!isReady) {
@@ -172,31 +181,33 @@ function App() {
* that is set up in the InnerApp component above.
*/
return (
<A11yProvider>
<KeyboardProvider enabled={false} statusBarTranslucent={true}>
<SessionProvider>
<PrefsStateProvider>
<I18nProvider>
<ShellStateProvider>
<InvitesStateProvider>
<ModalStateProvider>
<DialogStateProvider>
<LightboxStateProvider>
<PortalProvider>
<StarterPackProvider>
<InnerApp />
</StarterPackProvider>
</PortalProvider>
</LightboxStateProvider>
</DialogStateProvider>
</ModalStateProvider>
</InvitesStateProvider>
</ShellStateProvider>
</I18nProvider>
</PrefsStateProvider>
</SessionProvider>
</KeyboardProvider>
</A11yProvider>
<GeolocationProvider>
<A11yProvider>
<KeyboardProvider enabled={false} statusBarTranslucent={true}>
<SessionProvider>
<PrefsStateProvider>
<I18nProvider>
<ShellStateProvider>
<InvitesStateProvider>
<ModalStateProvider>
<DialogStateProvider>
<LightboxStateProvider>
<PortalProvider>
<StarterPackProvider>
<InnerApp />
</StarterPackProvider>
</PortalProvider>
</LightboxStateProvider>
</DialogStateProvider>
</ModalStateProvider>
</InvitesStateProvider>
</ShellStateProvider>
</I18nProvider>
</PrefsStateProvider>
</SessionProvider>
</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 DialogStateProvider} from '#/state/dialogs'
import {listenSessionDropped} from '#/state/events'
import {
beginResolveGeolocation,
ensureGeolocationResolved,
Provider as GeolocationProvider,
} from '#/state/geolocation'
import {Provider as InvitesStateProvider} from '#/state/invites'
import {Provider as LightboxStateProvider} from '#/state/lightbox'
import {MessagesProvider} from '#/state/messages'
@@ -71,6 +76,7 @@ function InnerApp() {
} finally {
setIsReady(true)
}
await ensureGeolocationResolved()
}
const account = readLastActiveAccount()
onLaunch(account)
@@ -139,7 +145,10 @@ function App() {
const [isReady, setReady] = useState(false)
React.useEffect(() => {
initPersistedState().then(() => setReady(true))
beginResolveGeolocation()
Promise.all([initPersistedState(), ensureGeolocationResolved()]).then(() =>
setReady(true),
)
}, [])
if (!isReady) {
@@ -151,29 +160,31 @@ function App() {
* that is set up in the InnerApp component above.
*/
return (
<A11yProvider>
<SessionProvider>
<PrefsStateProvider>
<I18nProvider>
<ShellStateProvider>
<InvitesStateProvider>
<ModalStateProvider>
<DialogStateProvider>
<LightboxStateProvider>
<PortalProvider>
<StarterPackProvider>
<InnerApp />
</StarterPackProvider>
</PortalProvider>
</LightboxStateProvider>
</DialogStateProvider>
</ModalStateProvider>
</InvitesStateProvider>
</ShellStateProvider>
</I18nProvider>
</PrefsStateProvider>
</SessionProvider>
</A11yProvider>
<GeolocationProvider>
<A11yProvider>
<SessionProvider>
<PrefsStateProvider>
<I18nProvider>
<ShellStateProvider>
<InvitesStateProvider>
<ModalStateProvider>
<DialogStateProvider>
<LightboxStateProvider>
<PortalProvider>
<StarterPackProvider>
<InnerApp />
</StarterPackProvider>
</PortalProvider>
</LightboxStateProvider>
</DialogStateProvider>
</ModalStateProvider>
</InvitesStateProvider>
</ShellStateProvider>
</I18nProvider>
</PrefsStateProvider>
</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)
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 {logger} from '#/logger'
import {device} from '#/storage'
export const ADDITIONAL_LABELER = 'did:plc:oz5zavafp7szpd2yyko57ccz'
export const ADDITIONAL_LABELERS_MAP = {
category: [ADDITIONAL_LABELER],
export const ADDITIONAL_LABELERS_MAP: {
[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 function isNonConfigurableModerationAuthority(did: string) {
@@ -11,10 +19,23 @@ export function isNonConfigurableModerationAuthority(did: string) {
}
export function configureAdditionalModerationAuthorities() {
BskyAgent.configure({
appLabelers: [
...BskyAgent.appLabelers,
...(ADDITIONAL_LABELERS_MAP.category ?? []),
],
const geolocation = device.get(['geolocation'])
let additionalLabelers: string[] = ALL_ADDITIONAL_LABELERS
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'
export * from '#/storage/schema'
/**
* Generic storage class. DO NOT use this directly. Instead, use the exported
* storage instances below.
@@ -69,4 +71,4 @@ export class Storage<Scopes extends unknown[], Schema> {
*
* `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
*/
export type Device = {}
export type Device = {
geolocation?: {
countryCode: string | undefined
}
}