diff --git a/src/App.native.tsx b/src/App.native.tsx
index 609d316d4b..ca16f85f3d 100644
--- a/src/App.native.tsx
+++ b/src/App.native.tsx
@@ -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 (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
)
}
diff --git a/src/App.web.tsx b/src/App.web.tsx
index 8531dc88d6..a3b0fc7702 100644
--- a/src/App.web.tsx
+++ b/src/App.web.tsx
@@ -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 (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
)
}
diff --git a/src/lib/geo.ts b/src/lib/geo.ts
deleted file mode 100644
index a51debc449..0000000000
--- a/src/lib/geo.ts
+++ /dev/null
@@ -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
-}
diff --git a/src/state/events.ts b/src/state/events.ts
index 08536e5062..dcd36464ec 100644
--- a/src/state/events.ts
+++ b/src/state/events.ts
@@ -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)
-}
diff --git a/src/state/geolocation.tsx b/src/state/geolocation.tsx
new file mode 100644
index 0000000000..a71761c733
--- /dev/null
+++ b/src/state/geolocation.tsx
@@ -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 {
+ 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 | 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({
+ 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 {children}
+}
+
+export function useGeolocation() {
+ return React.useContext(context)
+}
diff --git a/src/state/session/additional-moderation-authorities.ts b/src/state/session/additional-moderation-authorities.ts
index 64cce37ad2..6de51e8f70 100644
--- a/src/state/session/additional-moderation-authorities.ts
+++ b/src/state/session/additional-moderation-authorities.ts
@@ -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})
}
diff --git a/src/storage/index.ts b/src/storage/index.ts
index 819ffab7ec..ad5e58032f 100644
--- a/src/storage/index.ts
+++ b/src/storage/index.ts
@@ -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 {
*
* `device.set([key], true)`
*/
-export const device = new Storage<[], Device>({id: 'device'})
+export const device = new Storage<[], Device>({id: 'bsky_device'})
diff --git a/src/storage/schema.ts b/src/storage/schema.ts
index 6522d75a36..1f6f484774 100644
--- a/src/storage/schema.ts
+++ b/src/storage/schema.ts
@@ -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
+ }
+}