Override prefs in all locations

This commit is contained in:
Eric Bailey
2025-07-08 14:12:19 -05:00
parent 6dcac74691
commit c2c7138987
5 changed files with 104 additions and 39 deletions
+19 -6
View File
@@ -2,7 +2,7 @@ import {createContext, useContext, useMemo} from 'react'
import {type AppBskyUnspeccedDefs} from '@atproto/api' import {type AppBskyUnspeccedDefs} from '@atproto/api'
import {useQuery, useQueryClient} from '@tanstack/react-query' import {useQuery, useQueryClient} from '@tanstack/react-query'
import {wait} from '#/lib/async/wait' // import {wait} from '#/lib/async/wait'
import {isNetworkError} from '#/lib/strings/errors' import {isNetworkError} from '#/lib/strings/errors'
import {Logger} from '#/logger' import {Logger} from '#/logger'
import { import {
@@ -39,14 +39,27 @@ export function Provider({children}: {children: React.ReactNode}) {
const {geolocation} = useGeolocation() const {geolocation} = useGeolocation()
const {data, isFetched, refetch} = useQuery({ const {data, isFetched, refetch} = useQuery({
enabled: !!agent.session, /**
* This is load bearing. We always want this query to run and end in a
* "fetched" state, even if we fall back to defaults. This lets the rest of
* the app know that we've at least attempted to load the AA state.
*/
enabled: true,
queryKey: createAgeAssuranceQueryKey(agent.session?.did ?? 'never'), queryKey: createAgeAssuranceQueryKey(agent.session?.did ?? 'never'),
async queryFn() { async queryFn() {
if (!agent.session) return null
try { try {
const {data} = await wait( const {data} = await agent.app.bsky.unspecced.getAgeAssuranceState()
1e3, // const {data} = await wait(
agent.app.bsky.unspecced.getAgeAssuranceState(), // 1e3,
) // (() => ({
// data: {
// lastInitiatedAt: new Date().toISOString(),
// status: 'assured',
// } as AppBskyUnspeccedDefs.AgeAssuranceState,
// }))(),
// )
logger.debug(`fetch`, { logger.debug(`fetch`, {
data, data,
@@ -1,21 +1,42 @@
import {useCallback} from 'react' import {useCallback} from 'react'
import {type ModerationPrefs} from '@atproto/api' import {type ModerationPrefs} from '@atproto/api'
// import {Logger} from '#/logger'
import {useAgeAssuranceContext} from '#/state/age-assurance' import {useAgeAssuranceContext} from '#/state/age-assurance'
import {AGE_RESTRICTED_MODERATION_PREFS} from '#/state/age-assurance/const' import {AGE_RESTRICTED_MODERATION_PREFS} from '#/state/age-assurance/const'
// const logger = Logger.create(Logger.Context.AgeAssurance)
/**
* Hook to conditionally apply age-restricted moderation preferences, if
* needed. If not needed, the mod prefs passed to the callback will be used.
*/
export function useMaybeApplyAgeRestrictedModerationPrefs() { export function useMaybeApplyAgeRestrictedModerationPrefs() {
const {isLoaded, isAgeRestricted, isExempt} = useAgeAssuranceContext() const state = useAgeAssuranceContext()
return useCallback( return useCallback(
(prev: ModerationPrefs) => { (prev: ModerationPrefs) => {
const isDefinitelyAgeRestricted = isLoaded && isAgeRestricted const isDefinitelyAgeRestricted = state.isLoaded && state.isAgeRestricted
const notSureYet = !isLoaded && isAgeRestricted const notSureYet = !state.isLoaded && state.isAgeRestricted
if (isExempt) return prev if (state.isExempt) {
if (notSureYet || isDefinitelyAgeRestricted) // logger.debug('useMaybeApplyAgeRestrictedModerationPrefs: exempt', state)
return prev
}
if (notSureYet || isDefinitelyAgeRestricted) {
// logger.debug(
// 'useMaybeApplyAgeRestrictedModerationPrefs: overridden',
// state,
// )
return AGE_RESTRICTED_MODERATION_PREFS return AGE_RESTRICTED_MODERATION_PREFS
}
// logger.debug(
// 'useMaybeApplyAgeRestrictedModerationPrefs: not overridden',
// state,
// )
return prev return prev
}, },
[isLoaded, isAgeRestricted, isExempt], [state],
) )
} }
+23 -11
View File
@@ -1,19 +1,19 @@
import React from 'react' import React from 'react'
import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api' import {
type AppBskyActorDefs,
moderateProfile,
type ModerationOpts,
} from '@atproto/api'
import {keepPreviousData, useQuery, useQueryClient} from '@tanstack/react-query' import {keepPreviousData, useQuery, useQueryClient} from '@tanstack/react-query'
import {isJustAMute, moduiContainsHideableOffense} from '#/lib/moderation' import {isJustAMute, moduiContainsHideableOffense} from '#/lib/moderation'
import {logger} from '#/logger' import {logger} from '#/logger'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {STALE} from '#/state/queries' import {STALE} from '#/state/queries'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {useAgent} from '#/state/session' import {useAgent} from '#/state/session'
import {useModerationOpts} from '../preferences/moderation-opts'
import {DEFAULT_LOGGED_OUT_PREFERENCES} from './preferences' import {DEFAULT_LOGGED_OUT_PREFERENCES} from './preferences'
const DEFAULT_MOD_OPTS = {
userDid: undefined,
prefs: DEFAULT_LOGGED_OUT_PREFERENCES.moderationPrefs,
}
const RQKEY_ROOT = 'actor-autocomplete' const RQKEY_ROOT = 'actor-autocomplete'
export const RQKEY = (prefix: string) => [RQKEY_ROOT, prefix] export const RQKEY = (prefix: string) => [RQKEY_ROOT, prefix]
@@ -24,6 +24,7 @@ export function useActorAutocompleteQuery(
) { ) {
const moderationOpts = useModerationOpts() const moderationOpts = useModerationOpts()
const agent = useAgent() const agent = useAgent()
const {data: preferences} = usePreferencesQuery()
prefix = prefix.toLowerCase().trim() prefix = prefix.toLowerCase().trim()
if (prefix.endsWith('.')) { if (prefix.endsWith('.')) {
@@ -48,10 +49,15 @@ export function useActorAutocompleteQuery(
return computeSuggestions({ return computeSuggestions({
q: prefix, q: prefix,
searched: data, searched: data,
moderationOpts: moderationOpts || DEFAULT_MOD_OPTS, moderationOpts: moderationOpts || {
userDid: undefined,
prefs:
preferences?.moderationPrefs ||
DEFAULT_LOGGED_OUT_PREFERENCES.moderationPrefs,
},
}) })
}, },
[prefix, moderationOpts], [prefix, moderationOpts, preferences],
), ),
placeholderData: maintainData ? keepPreviousData : undefined, placeholderData: maintainData ? keepPreviousData : undefined,
}) })
@@ -62,6 +68,7 @@ export function useActorAutocompleteFn() {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const moderationOpts = useModerationOpts() const moderationOpts = useModerationOpts()
const agent = useAgent() const agent = useAgent()
const {data: preferences} = usePreferencesQuery()
return React.useCallback( return React.useCallback(
async ({query, limit = 8}: {query: string; limit?: number}) => { async ({query, limit = 8}: {query: string; limit?: number}) => {
@@ -88,10 +95,15 @@ export function useActorAutocompleteFn() {
return computeSuggestions({ return computeSuggestions({
q: query, q: query,
searched: res?.data.actors, searched: res?.data.actors,
moderationOpts: moderationOpts || DEFAULT_MOD_OPTS, moderationOpts: moderationOpts || {
userDid: undefined,
prefs:
preferences?.moderationPrefs ||
DEFAULT_LOGGED_OUT_PREFERENCES.moderationPrefs,
},
}) })
}, },
[queryClient, moderationOpts, agent], [queryClient, moderationOpts, agent, preferences],
) )
} }
+11 -3
View File
@@ -8,6 +8,7 @@ import {
type BskyAgent, type BskyAgent,
moderatePost, moderatePost,
type ModerationDecision, type ModerationDecision,
type ModerationPrefs,
} from '@atproto/api' } from '@atproto/api'
import { import {
type InfiniteData, type InfiniteData,
@@ -206,7 +207,11 @@ export function usePostFeedQuery(
* some not. * some not.
*/ */
if (!agent.session) { if (!agent.session) {
assertSomePostsPassModeration(res.feed) assertSomePostsPassModeration(
res.feed,
preferences?.moderationPrefs ||
DEFAULT_LOGGED_OUT_PREFERENCES.moderationPrefs,
)
} }
return { return {
@@ -596,7 +601,10 @@ export function* findAllProfilesInQueryData(
} }
} }
function assertSomePostsPassModeration(feed: AppBskyFeedDefs.FeedViewPost[]) { function assertSomePostsPassModeration(
feed: AppBskyFeedDefs.FeedViewPost[],
moderationPrefs: ModerationPrefs,
) {
// no posts in this feed // no posts in this feed
if (feed.length === 0) return true if (feed.length === 0) return true
@@ -606,7 +614,7 @@ function assertSomePostsPassModeration(feed: AppBskyFeedDefs.FeedViewPost[]) {
for (const item of feed) { for (const item of feed) {
const moderation = moderatePost(item.post, { const moderation = moderatePost(item.post, {
userDid: undefined, userDid: undefined,
prefs: DEFAULT_LOGGED_OUT_PREFERENCES.moderationPrefs, prefs: moderationPrefs,
}) })
if (!moderation.ui('contentList').filter) { if (!moderation.ui('contentList').filter) {
+24 -13
View File
@@ -1,3 +1,4 @@
import {useCallback} from 'react'
import { import {
type AppBskyActorDefs, type AppBskyActorDefs,
type BskyFeedViewPreference, type BskyFeedViewPreference,
@@ -9,6 +10,7 @@ import {PROD_DEFAULT_FEED} from '#/lib/constants'
import {replaceEqualDeep} from '#/lib/functions' import {replaceEqualDeep} from '#/lib/functions'
import {getAge} from '#/lib/strings/time' import {getAge} from '#/lib/strings/time'
import {logger} from '#/logger' import {logger} from '#/logger'
import {useAgeAssuranceContext} from '#/state/age-assurance'
import {useMaybeApplyAgeRestrictedModerationPrefs} from '#/state/age-assurance/useMaybeApplyAgeRestrictedModerationPrefs' import {useMaybeApplyAgeRestrictedModerationPrefs} from '#/state/age-assurance/useMaybeApplyAgeRestrictedModerationPrefs'
import {STALE} from '#/state/queries' import {STALE} from '#/state/queries'
import { import {
@@ -32,16 +34,23 @@ export const preferencesQueryKey = [preferencesQueryKeyRoot]
export function usePreferencesQuery() { export function usePreferencesQuery() {
const agent = useAgent() const agent = useAgent()
const overrideModPrefs = useMaybeApplyAgeRestrictedModerationPrefs() const {isLoaded: isAAStateLoaded} = useAgeAssuranceContext()
const query = useQuery({ const maybeOverrideModPrefs = useMaybeApplyAgeRestrictedModerationPrefs()
return useQuery({
/*
* We must await AA state, otherwise users may see a FOUC - esb
*/
enabled: isAAStateLoaded,
staleTime: STALE.SECONDS.FIFTEEN, staleTime: STALE.SECONDS.FIFTEEN,
structuralSharing: replaceEqualDeep, structuralSharing: replaceEqualDeep,
refetchOnWindowFocus: true, refetchOnWindowFocus: true,
queryKey: preferencesQueryKey, queryKey: preferencesQueryKey,
queryFn: async () => { queryFn: async () => {
if (!agent.did) { let preferences: UsePreferencesQueryResponse =
return DEFAULT_LOGGED_OUT_PREFERENCES DEFAULT_LOGGED_OUT_PREFERENCES
} else {
if (agent.did) {
const res = await agent.getPreferences() const res = await agent.getPreferences()
// save to local storage to ensure there are labels on initial requests // save to local storage to ensure there are labels on initial requests
@@ -50,7 +59,7 @@ export function usePreferencesQuery() {
res.moderationPrefs.labelers.map(l => l.did), res.moderationPrefs.labelers.map(l => l.did),
) )
const preferences: UsePreferencesQueryResponse = { preferences = {
...res, ...res,
savedFeeds: res.savedFeeds.filter(f => f.type !== 'unknown'), savedFeeds: res.savedFeeds.filter(f => f.type !== 'unknown'),
/** /**
@@ -67,16 +76,18 @@ export function usePreferencesQuery() {
}, },
userAge: res.birthDate ? getAge(res.birthDate) : undefined, userAge: res.birthDate ? getAge(res.birthDate) : undefined,
} }
return preferences
} }
return preferences
}, },
select: useCallback(
(data: UsePreferencesQueryResponse) => {
data.moderationPrefs = maybeOverrideModPrefs(data.moderationPrefs)
return data
},
[maybeOverrideModPrefs],
),
}) })
if (query.data) {
query.data.moderationPrefs = overrideModPrefs(query.data.moderationPrefs)
}
return query
} }
export function useClearPreferencesMutation() { export function useClearPreferencesMutation() {