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 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
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
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)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'})
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user