Working
This commit is contained in:
+37
-26
@@ -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
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
|
||||
@@ -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'})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user