diff --git a/src/state/age-assurance/index.tsx b/src/state/age-assurance/index.tsx index d909e2cd08..abb9bd1bdb 100644 --- a/src/state/age-assurance/index.tsx +++ b/src/state/age-assurance/index.tsx @@ -2,7 +2,7 @@ import {createContext, useContext, useMemo} from 'react' import {type AppBskyUnspeccedDefs} from '@atproto/api' 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 {Logger} from '#/logger' import { @@ -39,14 +39,27 @@ export function Provider({children}: {children: React.ReactNode}) { const {geolocation} = useGeolocation() 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'), async queryFn() { + if (!agent.session) return null + try { - const {data} = await wait( - 1e3, - agent.app.bsky.unspecced.getAgeAssuranceState(), - ) + const {data} = await agent.app.bsky.unspecced.getAgeAssuranceState() + // const {data} = await wait( + // 1e3, + // (() => ({ + // data: { + // lastInitiatedAt: new Date().toISOString(), + // status: 'assured', + // } as AppBskyUnspeccedDefs.AgeAssuranceState, + // }))(), + // ) logger.debug(`fetch`, { data, diff --git a/src/state/age-assurance/useMaybeApplyAgeRestrictedModerationPrefs.ts b/src/state/age-assurance/useMaybeApplyAgeRestrictedModerationPrefs.ts index 4d5bff6d3c..00a2ac369b 100644 --- a/src/state/age-assurance/useMaybeApplyAgeRestrictedModerationPrefs.ts +++ b/src/state/age-assurance/useMaybeApplyAgeRestrictedModerationPrefs.ts @@ -1,21 +1,42 @@ import {useCallback} from 'react' import {type ModerationPrefs} from '@atproto/api' +// import {Logger} from '#/logger' import {useAgeAssuranceContext} from '#/state/age-assurance' 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() { - const {isLoaded, isAgeRestricted, isExempt} = useAgeAssuranceContext() + const state = useAgeAssuranceContext() return useCallback( (prev: ModerationPrefs) => { - const isDefinitelyAgeRestricted = isLoaded && isAgeRestricted - const notSureYet = !isLoaded && isAgeRestricted + const isDefinitelyAgeRestricted = state.isLoaded && state.isAgeRestricted + const notSureYet = !state.isLoaded && state.isAgeRestricted - if (isExempt) return prev - if (notSureYet || isDefinitelyAgeRestricted) + if (state.isExempt) { + // logger.debug('useMaybeApplyAgeRestrictedModerationPrefs: exempt', state) + return prev + } + + if (notSureYet || isDefinitelyAgeRestricted) { + // logger.debug( + // 'useMaybeApplyAgeRestrictedModerationPrefs: overridden', + // state, + // ) return AGE_RESTRICTED_MODERATION_PREFS + } + + // logger.debug( + // 'useMaybeApplyAgeRestrictedModerationPrefs: not overridden', + // state, + // ) return prev }, - [isLoaded, isAgeRestricted, isExempt], + [state], ) } diff --git a/src/state/queries/actor-autocomplete.ts b/src/state/queries/actor-autocomplete.ts index acc0467715..43934e1c1a 100644 --- a/src/state/queries/actor-autocomplete.ts +++ b/src/state/queries/actor-autocomplete.ts @@ -1,19 +1,19 @@ 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 {isJustAMute, moduiContainsHideableOffense} from '#/lib/moderation' import {logger} from '#/logger' +import {useModerationOpts} from '#/state/preferences/moderation-opts' import {STALE} from '#/state/queries' +import {usePreferencesQuery} from '#/state/queries/preferences' import {useAgent} from '#/state/session' -import {useModerationOpts} from '../preferences/moderation-opts' import {DEFAULT_LOGGED_OUT_PREFERENCES} from './preferences' -const DEFAULT_MOD_OPTS = { - userDid: undefined, - prefs: DEFAULT_LOGGED_OUT_PREFERENCES.moderationPrefs, -} - const RQKEY_ROOT = 'actor-autocomplete' export const RQKEY = (prefix: string) => [RQKEY_ROOT, prefix] @@ -24,6 +24,7 @@ export function useActorAutocompleteQuery( ) { const moderationOpts = useModerationOpts() const agent = useAgent() + const {data: preferences} = usePreferencesQuery() prefix = prefix.toLowerCase().trim() if (prefix.endsWith('.')) { @@ -48,10 +49,15 @@ export function useActorAutocompleteQuery( return computeSuggestions({ q: prefix, 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, }) @@ -62,6 +68,7 @@ export function useActorAutocompleteFn() { const queryClient = useQueryClient() const moderationOpts = useModerationOpts() const agent = useAgent() + const {data: preferences} = usePreferencesQuery() return React.useCallback( async ({query, limit = 8}: {query: string; limit?: number}) => { @@ -88,10 +95,15 @@ export function useActorAutocompleteFn() { return computeSuggestions({ q: query, 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], ) } diff --git a/src/state/queries/post-feed.ts b/src/state/queries/post-feed.ts index 361081e67e..583b0432b1 100644 --- a/src/state/queries/post-feed.ts +++ b/src/state/queries/post-feed.ts @@ -8,6 +8,7 @@ import { type BskyAgent, moderatePost, type ModerationDecision, + type ModerationPrefs, } from '@atproto/api' import { type InfiniteData, @@ -206,7 +207,11 @@ export function usePostFeedQuery( * some not. */ if (!agent.session) { - assertSomePostsPassModeration(res.feed) + assertSomePostsPassModeration( + res.feed, + preferences?.moderationPrefs || + DEFAULT_LOGGED_OUT_PREFERENCES.moderationPrefs, + ) } 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 if (feed.length === 0) return true @@ -606,7 +614,7 @@ function assertSomePostsPassModeration(feed: AppBskyFeedDefs.FeedViewPost[]) { for (const item of feed) { const moderation = moderatePost(item.post, { userDid: undefined, - prefs: DEFAULT_LOGGED_OUT_PREFERENCES.moderationPrefs, + prefs: moderationPrefs, }) if (!moderation.ui('contentList').filter) { diff --git a/src/state/queries/preferences/index.ts b/src/state/queries/preferences/index.ts index a4025f0845..b21809fd70 100644 --- a/src/state/queries/preferences/index.ts +++ b/src/state/queries/preferences/index.ts @@ -1,3 +1,4 @@ +import {useCallback} from 'react' import { type AppBskyActorDefs, type BskyFeedViewPreference, @@ -9,6 +10,7 @@ import {PROD_DEFAULT_FEED} from '#/lib/constants' import {replaceEqualDeep} from '#/lib/functions' import {getAge} from '#/lib/strings/time' import {logger} from '#/logger' +import {useAgeAssuranceContext} from '#/state/age-assurance' import {useMaybeApplyAgeRestrictedModerationPrefs} from '#/state/age-assurance/useMaybeApplyAgeRestrictedModerationPrefs' import {STALE} from '#/state/queries' import { @@ -32,16 +34,23 @@ export const preferencesQueryKey = [preferencesQueryKeyRoot] export function usePreferencesQuery() { const agent = useAgent() - const overrideModPrefs = useMaybeApplyAgeRestrictedModerationPrefs() - const query = useQuery({ + const {isLoaded: isAAStateLoaded} = useAgeAssuranceContext() + const maybeOverrideModPrefs = useMaybeApplyAgeRestrictedModerationPrefs() + + return useQuery({ + /* + * We must await AA state, otherwise users may see a FOUC - esb + */ + enabled: isAAStateLoaded, staleTime: STALE.SECONDS.FIFTEEN, structuralSharing: replaceEqualDeep, refetchOnWindowFocus: true, queryKey: preferencesQueryKey, queryFn: async () => { - if (!agent.did) { - return DEFAULT_LOGGED_OUT_PREFERENCES - } else { + let preferences: UsePreferencesQueryResponse = + DEFAULT_LOGGED_OUT_PREFERENCES + + if (agent.did) { const res = await agent.getPreferences() // 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), ) - const preferences: UsePreferencesQueryResponse = { + preferences = { ...res, savedFeeds: res.savedFeeds.filter(f => f.type !== 'unknown'), /** @@ -67,16 +76,18 @@ export function usePreferencesQuery() { }, 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() {