Additional moderation (#5172)
* Set up additional mod authorities * Filter out non-configurable mod authorities * WIP * Working * Cleanup, add mod * Cleanup * Add more debug logs * Tweak logs * Filter out imperative labels from typeaheads * Filter hideable content from notifications * Add api * Fall back in dev * Remove space * Use prod endpoint * Add tiny notice * Add notice to labeler card, show all labelers
This commit is contained in:
+45
-31
@@ -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'
|
||||||
@@ -66,6 +71,11 @@ import {BackgroundNotificationPreferencesProvider} from '../modules/expo-backgro
|
|||||||
|
|
||||||
SplashScreen.preventAutoHideAsync()
|
SplashScreen.preventAutoHideAsync()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Begin geolocation ASAP
|
||||||
|
*/
|
||||||
|
beginResolveGeolocation()
|
||||||
|
|
||||||
function InnerApp() {
|
function InnerApp() {
|
||||||
const [isReady, setIsReady] = React.useState(false)
|
const [isReady, setIsReady] = React.useState(false)
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
@@ -158,7 +168,9 @@ function App() {
|
|||||||
const [isReady, setReady] = useState(false)
|
const [isReady, setReady] = useState(false)
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
initPersistedState().then(() => setReady(true))
|
Promise.all([initPersistedState(), ensureGeolocationResolved()]).then(() =>
|
||||||
|
setReady(true),
|
||||||
|
)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
if (!isReady) {
|
if (!isReady) {
|
||||||
@@ -170,36 +182,38 @@ 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>
|
||||||
<SafeAreaProvider
|
<StarterPackProvider>
|
||||||
initialMetrics={initialWindowMetrics}>
|
<SafeAreaProvider
|
||||||
<IntentDialogProvider>
|
initialMetrics={initialWindowMetrics}>
|
||||||
<InnerApp />
|
<IntentDialogProvider>
|
||||||
</IntentDialogProvider>
|
<InnerApp />
|
||||||
</SafeAreaProvider>
|
</IntentDialogProvider>
|
||||||
</StarterPackProvider>
|
</SafeAreaProvider>
|
||||||
</PortalProvider>
|
</StarterPackProvider>
|
||||||
</LightboxStateProvider>
|
</PortalProvider>
|
||||||
</DialogStateProvider>
|
</LightboxStateProvider>
|
||||||
</ModalStateProvider>
|
</DialogStateProvider>
|
||||||
</InvitesStateProvider>
|
</ModalStateProvider>
|
||||||
</ShellStateProvider>
|
</InvitesStateProvider>
|
||||||
</I18nProvider>
|
</ShellStateProvider>
|
||||||
</PrefsStateProvider>
|
</I18nProvider>
|
||||||
</SessionProvider>
|
</PrefsStateProvider>
|
||||||
</KeyboardProvider>
|
</SessionProvider>
|
||||||
</A11yProvider>
|
</KeyboardProvider>
|
||||||
|
</A11yProvider>
|
||||||
|
</GeolocationProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+40
-26
@@ -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'
|
||||||
@@ -54,6 +59,11 @@ import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialo
|
|||||||
import {Provider as PortalProvider} from '#/components/Portal'
|
import {Provider as PortalProvider} from '#/components/Portal'
|
||||||
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
|
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Begin geolocation ASAP
|
||||||
|
*/
|
||||||
|
beginResolveGeolocation()
|
||||||
|
|
||||||
function InnerApp() {
|
function InnerApp() {
|
||||||
const [isReady, setIsReady] = React.useState(false)
|
const [isReady, setIsReady] = React.useState(false)
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
@@ -148,7 +158,9 @@ function App() {
|
|||||||
const [isReady, setReady] = useState(false)
|
const [isReady, setReady] = useState(false)
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
initPersistedState().then(() => setReady(true))
|
Promise.all([initPersistedState(), ensureGeolocationResolved()]).then(() =>
|
||||||
|
setReady(true),
|
||||||
|
)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
if (!isReady) {
|
if (!isReady) {
|
||||||
@@ -160,31 +172,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>
|
||||||
<SessionProvider>
|
<A11yProvider>
|
||||||
<PrefsStateProvider>
|
<SessionProvider>
|
||||||
<I18nProvider>
|
<PrefsStateProvider>
|
||||||
<ShellStateProvider>
|
<I18nProvider>
|
||||||
<InvitesStateProvider>
|
<ShellStateProvider>
|
||||||
<ModalStateProvider>
|
<InvitesStateProvider>
|
||||||
<DialogStateProvider>
|
<ModalStateProvider>
|
||||||
<LightboxStateProvider>
|
<DialogStateProvider>
|
||||||
<PortalProvider>
|
<LightboxStateProvider>
|
||||||
<StarterPackProvider>
|
<PortalProvider>
|
||||||
<IntentDialogProvider>
|
<StarterPackProvider>
|
||||||
<InnerApp />
|
<IntentDialogProvider>
|
||||||
</IntentDialogProvider>
|
<InnerApp />
|
||||||
</StarterPackProvider>
|
</IntentDialogProvider>
|
||||||
</PortalProvider>
|
</StarterPackProvider>
|
||||||
</LightboxStateProvider>
|
</PortalProvider>
|
||||||
</DialogStateProvider>
|
</LightboxStateProvider>
|
||||||
</ModalStateProvider>
|
</DialogStateProvider>
|
||||||
</InvitesStateProvider>
|
</ModalStateProvider>
|
||||||
</ShellStateProvider>
|
</InvitesStateProvider>
|
||||||
</I18nProvider>
|
</ShellStateProvider>
|
||||||
</PrefsStateProvider>
|
</I18nProvider>
|
||||||
</SessionProvider>
|
</PrefsStateProvider>
|
||||||
</A11yProvider>
|
</SessionProvider>
|
||||||
|
</A11yProvider>
|
||||||
|
</GeolocationProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {sanitizeHandle} from '#/lib/strings/handles'
|
|||||||
import {useLabelerInfoQuery} from '#/state/queries/labeler'
|
import {useLabelerInfoQuery} from '#/state/queries/labeler'
|
||||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
import {atoms as a, useTheme, ViewStyleProp} from '#/alf'
|
import {atoms as a, useTheme, ViewStyleProp} from '#/alf'
|
||||||
|
import {Flag_Stroke2_Corner0_Rounded as Flag} from '#/components/icons/Flag'
|
||||||
import {Link as InternalLink, LinkProps} from '#/components/Link'
|
import {Link as InternalLink, LinkProps} from '#/components/Link'
|
||||||
import {RichText} from '#/components/RichText'
|
import {RichText} from '#/components/RichText'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
@@ -43,21 +44,40 @@ export function Avatar({avatar}: {avatar?: string}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Title({value}: {value: string}) {
|
export function Title({value}: {value: string}) {
|
||||||
return <Text style={[a.text_md, a.font_bold]}>{value}</Text>
|
return <Text style={[a.text_md, a.font_bold, a.leading_tight]}>{value}</Text>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Description({value, handle}: {value?: string; handle: string}) {
|
export function Description({value, handle}: {value?: string; handle: string}) {
|
||||||
return value ? (
|
return value ? (
|
||||||
<Text numberOfLines={2}>
|
<Text numberOfLines={2}>
|
||||||
<RichText value={value} style={[]} />
|
<RichText value={value} style={[a.leading_snug]} />
|
||||||
</Text>
|
</Text>
|
||||||
) : (
|
) : (
|
||||||
<Text>
|
<Text style={[a.leading_snug]}>
|
||||||
<Trans>By {sanitizeHandle(handle, '@')}</Trans>
|
<Trans>By {sanitizeHandle(handle, '@')}</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function RegionalNotice() {
|
||||||
|
const t = useTheme()
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_row,
|
||||||
|
a.align_center,
|
||||||
|
a.gap_xs,
|
||||||
|
a.pt_2xs,
|
||||||
|
{marginLeft: -2},
|
||||||
|
]}>
|
||||||
|
<Flag fill={t.atoms.text_contrast_low.color} size="sm" />
|
||||||
|
<Text style={[a.italic, a.leading_snug]}>
|
||||||
|
<Trans>Required in your region</Trans>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function LikeCount({count}: {count: number}) {
|
export function LikeCount({count}: {count: number}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
return (
|
return (
|
||||||
@@ -85,7 +105,7 @@ export function Content({children}: React.PropsWithChildren<{}>) {
|
|||||||
a.align_center,
|
a.align_center,
|
||||||
a.justify_between,
|
a.justify_between,
|
||||||
]}>
|
]}>
|
||||||
<View style={[a.gap_xs, a.flex_1]}>{children}</View>
|
<View style={[a.gap_2xs, a.flex_1]}>{children}</View>
|
||||||
|
|
||||||
<ChevronRight size="md" style={[a.z_10, t.atoms.text_contrast_low]} />
|
<ChevronRight size="md" style={[a.z_10, t.atoms.text_contrast_low]} />
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -33,6 +33,20 @@ export function isJustAMute(modui: ModerationUI): boolean {
|
|||||||
return modui.filters.length === 1 && modui.filters[0].type === 'muted'
|
return modui.filters.length === 1 && modui.filters[0].type === 'muted'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function moduiContainsHideableOffense(modui: ModerationUI): boolean {
|
||||||
|
const label = modui.filters.at(0)
|
||||||
|
if (label && label.type === 'label') {
|
||||||
|
return labelIsHideableOffense(label.label)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
export function labelIsHideableOffense(
|
||||||
|
label: ComAtprotoLabelDefs.Label,
|
||||||
|
): boolean {
|
||||||
|
return ['!hide', '!takedown'].includes(label.val)
|
||||||
|
}
|
||||||
|
|
||||||
export function getLabelingServiceTitle({
|
export function getLabelingServiceTitle({
|
||||||
displayName,
|
displayName,
|
||||||
handle,
|
handle,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {msg, Trans} from '@lingui/macro'
|
|||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {useFocusEffect} from '@react-navigation/native'
|
import {useFocusEffect} from '@react-navigation/native'
|
||||||
|
|
||||||
|
import {useAnalytics} from '#/lib/analytics/analytics'
|
||||||
import {getLabelingServiceTitle} from '#/lib/moderation'
|
import {getLabelingServiceTitle} from '#/lib/moderation'
|
||||||
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
|
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
@@ -22,8 +23,8 @@ import {
|
|||||||
useProfileUpdateMutation,
|
useProfileUpdateMutation,
|
||||||
} from '#/state/queries/profile'
|
} from '#/state/queries/profile'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
|
import {isNonConfigurableModerationAuthority} from '#/state/session/additional-moderation-authorities'
|
||||||
import {useSetMinimalShellMode} from '#/state/shell'
|
import {useSetMinimalShellMode} from '#/state/shell'
|
||||||
import {useAnalytics} from 'lib/analytics/analytics'
|
|
||||||
import {ViewHeader} from '#/view/com/util/ViewHeader'
|
import {ViewHeader} from '#/view/com/util/ViewHeader'
|
||||||
import {CenteredView} from '#/view/com/util/Views'
|
import {CenteredView} from '#/view/com/util/Views'
|
||||||
import {ScrollView} from '#/view/com/util/Views'
|
import {ScrollView} from '#/view/com/util/Views'
|
||||||
@@ -455,6 +456,9 @@ export function ModerationScreenInner({
|
|||||||
value={labeler.creator.description}
|
value={labeler.creator.description}
|
||||||
handle={labeler.creator.handle}
|
handle={labeler.creator.handle}
|
||||||
/>
|
/>
|
||||||
|
{isNonConfigurableModerationAuthority(
|
||||||
|
labeler.creator.did,
|
||||||
|
) && <LabelingService.RegionalNotice />}
|
||||||
</LabelingService.Content>
|
</LabelingService.Content>
|
||||||
</LabelingService.Outer>
|
</LabelingService.Outer>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import EventEmitter from 'eventemitter3'
|
||||||
|
|
||||||
|
import {networkRetry} from '#/lib/async/retry'
|
||||||
|
import {logger} from '#/logger'
|
||||||
|
import {IS_DEV} from '#/env'
|
||||||
|
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://bsky.app/ipcc`)
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`geolocation: lookup failed ${res.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const json = await res.json()
|
||||||
|
|
||||||
|
if (json.countryCode) {
|
||||||
|
return {
|
||||||
|
countryCode: json.countryCode,
|
||||||
|
}
|
||||||
|
} 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() {
|
||||||
|
/**
|
||||||
|
* In dev, IP server is unavailable, so we just set the default geolocation
|
||||||
|
* and fail closed.
|
||||||
|
*/
|
||||||
|
if (IS_DEV) {
|
||||||
|
geolocationResolution = new Promise(y => y())
|
||||||
|
device.set(['geolocation'], DEFAULT_GEOLOCATION)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
geolocationResolution = new Promise(async resolve => {
|
||||||
|
try {
|
||||||
|
// Try once, fail fast
|
||||||
|
const geolocation = await getGeolocation()
|
||||||
|
if (geolocation) {
|
||||||
|
device.set(['geolocation'], geolocation)
|
||||||
|
emitGeolocationUpdate(geolocation)
|
||||||
|
logger.debug(`geolocation: success`, {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)
|
||||||
|
logger.debug(`geolocation: success`, {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.debug(`geolocation: using cache`, {cached})
|
||||||
|
} else {
|
||||||
|
logger.debug(`geolocation: no cache`)
|
||||||
|
await geolocationResolution
|
||||||
|
logger.debug(`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)
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ import React from 'react'
|
|||||||
import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api'
|
import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api'
|
||||||
import {keepPreviousData, useQuery, useQueryClient} from '@tanstack/react-query'
|
import {keepPreviousData, useQuery, useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {isJustAMute} from '#/lib/moderation'
|
import {isJustAMute, moduiContainsHideableOffense} from '#/lib/moderation'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {STALE} from '#/state/queries'
|
import {STALE} from '#/state/queries'
|
||||||
import {useAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
@@ -113,6 +113,10 @@ function computeSuggestions({
|
|||||||
return items.filter(profile => {
|
return items.filter(profile => {
|
||||||
const modui = moderateProfile(profile, moderationOpts).ui('profileList')
|
const modui = moderateProfile(profile, moderationOpts).ui('profileList')
|
||||||
const isExactMatch = q && profile.handle.toLowerCase() === q
|
const isExactMatch = q && profile.handle.toLowerCase() === q
|
||||||
return isExactMatch || !modui.filter || isJustAMute(modui)
|
return (
|
||||||
|
(isExactMatch && !moduiContainsHideableOffense(modui)) ||
|
||||||
|
!modui.filter ||
|
||||||
|
isJustAMute(modui)
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
import {QueryClient} from '@tanstack/react-query'
|
import {QueryClient} from '@tanstack/react-query'
|
||||||
import chunk from 'lodash.chunk'
|
import chunk from 'lodash.chunk'
|
||||||
|
|
||||||
|
import {labelIsHideableOffense} from '#/lib/moderation'
|
||||||
import {precacheProfile} from '../profile'
|
import {precacheProfile} from '../profile'
|
||||||
import {FeedNotification, FeedPage, NotificationType} from './types'
|
import {FeedNotification, FeedPage, NotificationType} from './types'
|
||||||
|
|
||||||
@@ -104,6 +105,10 @@ export function shouldFilterNotif(
|
|||||||
notif: AppBskyNotificationListNotifications.Notification,
|
notif: AppBskyNotificationListNotifications.Notification,
|
||||||
moderationOpts: ModerationOpts | undefined,
|
moderationOpts: ModerationOpts | undefined,
|
||||||
): boolean {
|
): boolean {
|
||||||
|
const containsImperative = !!notif.author.labels?.some(labelIsHideableOffense)
|
||||||
|
if (containsImperative) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
if (!moderationOpts) {
|
if (!moderationOpts) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import {BskyAgent} from '@atproto/api'
|
||||||
|
|
||||||
|
import {logger} from '#/logger'
|
||||||
|
import {device} from '#/storage'
|
||||||
|
|
||||||
|
export const BR_LABELER = 'did:plc:ekitcvx7uwnauoqy5oest3hm'
|
||||||
|
export const ADDITIONAL_LABELERS_MAP: {
|
||||||
|
[countryCode: string]: string[]
|
||||||
|
} = {
|
||||||
|
BR: [BR_LABELER],
|
||||||
|
}
|
||||||
|
export const ALL_ADDITIONAL_LABELERS = Object.values(
|
||||||
|
ADDITIONAL_LABELERS_MAP,
|
||||||
|
).flat()
|
||||||
|
export const NON_CONFIGURABLE_LABELERS = [BR_LABELER]
|
||||||
|
|
||||||
|
export function isNonConfigurableModerationAuthority(did: string) {
|
||||||
|
return NON_CONFIGURABLE_LABELERS.includes(did)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function configureAdditionalModerationAuthorities() {
|
||||||
|
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.info(`applying mod authorities`, {
|
||||||
|
additionalLabelers,
|
||||||
|
appLabelers,
|
||||||
|
})
|
||||||
|
|
||||||
|
BskyAgent.configure({appLabelers})
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import {BSKY_LABELER_DID, BskyAgent} from '@atproto/api'
|
import {BSKY_LABELER_DID, BskyAgent} from '@atproto/api'
|
||||||
|
|
||||||
import {IS_TEST_USER} from '#/lib/constants'
|
import {IS_TEST_USER} from '#/lib/constants'
|
||||||
|
import {configureAdditionalModerationAuthorities} from './additional-moderation-authorities'
|
||||||
import {readLabelers} from './agent-config'
|
import {readLabelers} from './agent-config'
|
||||||
import {SessionAccount} from './types'
|
import {SessionAccount} from './types'
|
||||||
|
|
||||||
@@ -8,6 +9,7 @@ export function configureModerationForGuest() {
|
|||||||
// This global mutation is *only* OK because this code is only relevant for testing.
|
// This global mutation is *only* OK because this code is only relevant for testing.
|
||||||
// Don't add any other global behavior here!
|
// Don't add any other global behavior here!
|
||||||
switchToBskyAppLabeler()
|
switchToBskyAppLabeler()
|
||||||
|
configureAdditionalModerationAuthorities()
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function configureModerationForAccount(
|
export async function configureModerationForAccount(
|
||||||
@@ -31,6 +33,8 @@ export async function configureModerationForAccount(
|
|||||||
// If there are no headers in the storage, we'll not send them on the initial requests.
|
// If there are no headers in the storage, we'll not send them on the initial requests.
|
||||||
// If we wanted to fix this, we could block on the preferences query here.
|
// If we wanted to fix this, we could block on the preferences query here.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
configureAdditionalModerationAuthorities()
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchToBskyAppLabeler() {
|
function switchToBskyAppLabeler() {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {MMKV} from 'react-native-mmkv'
|
import {MMKV} from 'react-native-mmkv'
|
||||||
|
|
||||||
|
import {IS_DEV} from '#/env'
|
||||||
import {Device} from '#/storage/schema'
|
import {Device} from '#/storage/schema'
|
||||||
|
|
||||||
export * from '#/storage/schema'
|
export * from '#/storage/schema'
|
||||||
@@ -71,4 +72,11 @@ 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'})
|
||||||
|
|
||||||
|
if (IS_DEV && typeof window !== 'undefined') {
|
||||||
|
// @ts-ignore
|
||||||
|
window.bsky_storage = {
|
||||||
|
device,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,4 +5,7 @@ export type Device = {
|
|||||||
fontScale: '-2' | '-1' | '0' | '1' | '2'
|
fontScale: '-2' | '-1' | '0' | '1' | '2'
|
||||||
fontFamily: 'system' | 'theme'
|
fontFamily: 'system' | 'theme'
|
||||||
lastNuxDialog: string | undefined
|
lastNuxDialog: string | undefined
|
||||||
|
geolocation?: {
|
||||||
|
countryCode: string | undefined
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user