Files
bsky-social-app/src/state/queries/preferences/index.ts
T
2025-12-11 09:39:15 -06:00

438 lines
12 KiB
TypeScript

import {useCallback} from 'react'
import {
type AppBskyActorDefs,
type BskyFeedViewPreference,
type LabelPreference,
} from '@atproto/api'
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
import {PROD_DEFAULT_FEED} from '#/lib/constants'
import {replaceEqualDeep} from '#/lib/functions'
import {getAge} from '#/lib/strings/time'
import {logger} from '#/logger'
import {STALE} from '#/state/queries'
import {
DEFAULT_HOME_FEED_PREFS,
DEFAULT_LOGGED_OUT_PREFERENCES,
DEFAULT_THREAD_VIEW_PREFS,
} from '#/state/queries/preferences/const'
import {
type ThreadViewPreferences,
type UsePreferencesQueryResponse,
} from '#/state/queries/preferences/types'
import {useAgent} from '#/state/session'
import {saveLabelers} from '#/state/session/agent-config'
import {useAgeAssurance} from '#/ageAssurance'
import {makeAgeRestrictedModerationPrefs} from '#/ageAssurance/util'
export * from '#/state/queries/preferences/const'
export * from '#/state/queries/preferences/moderation'
export * from '#/state/queries/preferences/types'
const preferencesQueryKeyRoot = 'getPreferences'
export const preferencesQueryKey = [preferencesQueryKeyRoot]
export function usePreferencesQuery() {
const agent = useAgent()
const aa = useAgeAssurance()
return useQuery({
staleTime: STALE.SECONDS.FIFTEEN,
structuralSharing: replaceEqualDeep,
refetchOnWindowFocus: true,
queryKey: preferencesQueryKey,
queryFn: async () => {
if (!agent.did) {
return DEFAULT_LOGGED_OUT_PREFERENCES
} else {
const res = await agent.getPreferences()
// save to local storage to ensure there are labels on initial requests
saveLabelers(
agent.did,
res.moderationPrefs.labelers.map(l => l.did),
)
const preferences: UsePreferencesQueryResponse = {
...res,
savedFeeds: res.savedFeeds.filter(f => f.type !== 'unknown'),
/**
* Special preference, only used for following feed, previously
* called `home`
*/
feedViewPrefs: {
...DEFAULT_HOME_FEED_PREFS,
...(res.feedViewPrefs.home || {}),
},
threadViewPrefs: {
...DEFAULT_THREAD_VIEW_PREFS,
...(res.threadViewPrefs ?? {}),
},
userAge: res.birthDate ? getAge(res.birthDate) : undefined,
}
return preferences
}
},
select: useCallback(
(data: UsePreferencesQueryResponse) => {
/**
* Prefs are all downstream of age assurance now. For logged-out
* users, we override moderation prefs based on AA state.
*/
if (aa.state.access !== aa.Access.Full) {
data = {
...data,
moderationPrefs: makeAgeRestrictedModerationPrefs(
data.moderationPrefs,
),
}
}
return data
},
[aa],
),
})
}
export function useClearPreferencesMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
return useMutation({
mutationFn: async () => {
await agent.app.bsky.actor.putPreferences({preferences: []})
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
})
},
})
}
export function usePreferencesSetContentLabelMutation() {
const agent = useAgent()
const queryClient = useQueryClient()
return useMutation<
void,
unknown,
{label: string; visibility: LabelPreference; labelerDid: string | undefined}
>({
mutationFn: async ({label, visibility, labelerDid}) => {
await agent.setContentLabelPref(label, visibility, labelerDid)
logger.metric(
'moderation:changeLabelPreference',
{preference: visibility},
{statsig: true},
)
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
})
},
})
}
export function useSetContentLabelMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
return useMutation({
mutationFn: async ({
label,
visibility,
labelerDid,
}: {
label: string
visibility: LabelPreference
labelerDid?: string
}) => {
await agent.setContentLabelPref(label, visibility, labelerDid)
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
})
},
})
}
export function usePreferencesSetAdultContentMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
return useMutation<void, unknown, {enabled: boolean}>({
mutationFn: async ({enabled}) => {
await agent.setAdultContentEnabled(enabled)
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
})
},
})
}
export function useSetFeedViewPreferencesMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
return useMutation<void, unknown, Partial<BskyFeedViewPreference>>({
mutationFn: async prefs => {
/*
* special handling here, merged into `feedViewPrefs` above, since
* following was previously called `home`
*/
await agent.setFeedViewPrefs('home', prefs)
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
})
},
})
}
export function useSetThreadViewPreferencesMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
return useMutation<void, unknown, Partial<ThreadViewPreferences>>({
mutationFn: async prefs => {
await agent.setThreadViewPrefs(prefs)
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
})
},
})
}
export function useOverwriteSavedFeedsMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
return useMutation<void, unknown, AppBskyActorDefs.SavedFeed[]>({
mutationFn: async savedFeeds => {
await agent.overwriteSavedFeeds(savedFeeds)
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
})
},
})
}
export function useAddSavedFeedsMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
return useMutation<
void,
unknown,
Pick<AppBskyActorDefs.SavedFeed, 'type' | 'value' | 'pinned'>[]
>({
mutationFn: async savedFeeds => {
await agent.addSavedFeeds(savedFeeds)
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
})
},
})
}
export function useRemoveFeedMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
return useMutation<void, unknown, Pick<AppBskyActorDefs.SavedFeed, 'id'>>({
mutationFn: async savedFeed => {
await agent.removeSavedFeeds([savedFeed.id])
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
})
},
})
}
export function useReplaceForYouWithDiscoverFeedMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
return useMutation({
mutationFn: async ({
forYouFeedConfig,
discoverFeedConfig,
}: {
forYouFeedConfig: AppBskyActorDefs.SavedFeed | undefined
discoverFeedConfig: AppBskyActorDefs.SavedFeed | undefined
}) => {
if (forYouFeedConfig) {
await agent.removeSavedFeeds([forYouFeedConfig.id])
}
if (!discoverFeedConfig) {
await agent.addSavedFeeds([
{
type: 'feed',
value: PROD_DEFAULT_FEED('whats-hot'),
pinned: true,
},
])
} else {
await agent.updateSavedFeeds([
{
...discoverFeedConfig,
pinned: true,
},
])
}
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
})
},
})
}
export function useUpdateSavedFeedsMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
return useMutation<void, unknown, AppBskyActorDefs.SavedFeed[]>({
mutationFn: async feeds => {
await agent.updateSavedFeeds(feeds)
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
})
},
})
}
export function useUpsertMutedWordsMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
return useMutation({
mutationFn: async (mutedWords: AppBskyActorDefs.MutedWord[]) => {
await agent.upsertMutedWords(mutedWords)
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
})
},
})
}
export function useUpdateMutedWordMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
return useMutation({
mutationFn: async (mutedWord: AppBskyActorDefs.MutedWord) => {
await agent.updateMutedWord(mutedWord)
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
})
},
})
}
export function useRemoveMutedWordMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
return useMutation({
mutationFn: async (mutedWord: AppBskyActorDefs.MutedWord) => {
await agent.removeMutedWord(mutedWord)
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
})
},
})
}
export function useRemoveMutedWordsMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
return useMutation({
mutationFn: async (mutedWords: AppBskyActorDefs.MutedWord[]) => {
await agent.removeMutedWords(mutedWords)
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
})
},
})
}
export function useQueueNudgesMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
return useMutation({
mutationFn: async (nudges: string | string[]) => {
await agent.bskyAppQueueNudges(nudges)
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
})
},
})
}
export function useDismissNudgesMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
return useMutation({
mutationFn: async (nudges: string | string[]) => {
await agent.bskyAppDismissNudges(nudges)
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
})
},
})
}
export function useSetActiveProgressGuideMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
return useMutation({
mutationFn: async (
guide: AppBskyActorDefs.BskyAppProgressGuide | undefined,
) => {
await agent.bskyAppSetActiveProgressGuide(guide)
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
})
},
})
}
export function useSetVerificationPrefsMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
return useMutation<void, unknown, AppBskyActorDefs.VerificationPrefs>({
mutationFn: async prefs => {
await agent.setVerificationPrefs(prefs)
if (prefs.hideBadges) {
logger.metric('verification:settings:hideBadges', {}, {statsig: true})
} else {
logger.metric('verification:settings:unHideBadges', {}, {statsig: true})
}
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
})
},
})
}