Eric/preferences (#1873)
* Add initial preferences query, couple mutations * Remove unused * Clean up labels, migrate getModerationOpts * Add birth date handling * Migrate feed prefs * Migrate thread view prefs * Migrate homeFeed to use existing key name * Fix up saved feeds in response, no impl yet * Migrate saved feeds to new hooks * Clean up more of preferences * Fix PreferencesThreads load state * Fix modal dismissal * Small spacing fix --------- Co-authored-by: Paul Frazee <pfrazee@gmail.com>
This commit is contained in:
parent
c8c308e31e
commit
05b728fffc
22 changed files with 1339 additions and 914 deletions
106
src/state/queries/feed.ts
Normal file
106
src/state/queries/feed.ts
Normal file
|
@ -0,0 +1,106 @@
|
|||
import {useQuery} from '@tanstack/react-query'
|
||||
import {AtUri, RichText, AppBskyFeedDefs, AppBskyGraphDefs} from '@atproto/api'
|
||||
|
||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {useSession} from '#/state/session'
|
||||
|
||||
type FeedSourceInfo =
|
||||
| {
|
||||
type: 'feed'
|
||||
uri: string
|
||||
cid: string
|
||||
avatar: string | undefined
|
||||
displayName: string
|
||||
description: RichText
|
||||
creatorDid: string
|
||||
creatorHandle: string
|
||||
likeCount: number | undefined
|
||||
likeUri: string | undefined
|
||||
}
|
||||
| {
|
||||
type: 'list'
|
||||
uri: string
|
||||
cid: string
|
||||
avatar: string | undefined
|
||||
displayName: string
|
||||
description: RichText
|
||||
creatorDid: string
|
||||
creatorHandle: string
|
||||
}
|
||||
|
||||
export const useFeedSourceInfoQueryKey = ({uri}: {uri: string}) => [
|
||||
'getFeedSourceInfo',
|
||||
uri,
|
||||
]
|
||||
|
||||
const feedSourceNSIDs = {
|
||||
feed: 'app.bsky.feed.generator',
|
||||
list: 'app.bsky.graph.list',
|
||||
}
|
||||
|
||||
function hydrateFeedGenerator(
|
||||
view: AppBskyFeedDefs.GeneratorView,
|
||||
): FeedSourceInfo {
|
||||
return {
|
||||
type: 'feed',
|
||||
uri: view.uri,
|
||||
cid: view.cid,
|
||||
avatar: view.avatar,
|
||||
displayName: view.displayName
|
||||
? sanitizeDisplayName(view.displayName)
|
||||
: `Feed by ${sanitizeHandle(view.creator.handle, '@')}`,
|
||||
description: new RichText({
|
||||
text: view.description || '',
|
||||
facets: (view.descriptionFacets || [])?.slice(),
|
||||
}),
|
||||
creatorDid: view.creator.did,
|
||||
creatorHandle: view.creator.handle,
|
||||
likeCount: view.likeCount,
|
||||
likeUri: view.viewer?.like,
|
||||
}
|
||||
}
|
||||
|
||||
function hydrateList(view: AppBskyGraphDefs.ListView): FeedSourceInfo {
|
||||
return {
|
||||
type: 'list',
|
||||
uri: view.uri,
|
||||
cid: view.cid,
|
||||
avatar: view.avatar,
|
||||
description: new RichText({
|
||||
text: view.description || '',
|
||||
facets: (view.descriptionFacets || [])?.slice(),
|
||||
}),
|
||||
creatorDid: view.creator.did,
|
||||
creatorHandle: view.creator.handle,
|
||||
displayName: view.name
|
||||
? sanitizeDisplayName(view.name)
|
||||
: `User List by ${sanitizeHandle(view.creator.handle, '@')}`,
|
||||
}
|
||||
}
|
||||
|
||||
export function useFeedSourceInfoQuery({uri}: {uri: string}) {
|
||||
const {agent} = useSession()
|
||||
const {pathname} = new AtUri(uri)
|
||||
const type = pathname.includes(feedSourceNSIDs.feed) ? 'feed' : 'list'
|
||||
|
||||
return useQuery({
|
||||
queryKey: useFeedSourceInfoQueryKey({uri}),
|
||||
queryFn: async () => {
|
||||
let view: FeedSourceInfo
|
||||
|
||||
if (type === 'feed') {
|
||||
const res = await agent.app.bsky.feed.getFeedGenerator({feed: uri})
|
||||
view = hydrateFeedGenerator(res.data.view)
|
||||
} else {
|
||||
const res = await agent.app.bsky.graph.getList({
|
||||
list: uri,
|
||||
limit: 1,
|
||||
})
|
||||
view = hydrateList(res.data.list)
|
||||
}
|
||||
|
||||
return view
|
||||
},
|
||||
})
|
||||
}
|
27
src/state/queries/preferences/const.ts
Normal file
27
src/state/queries/preferences/const.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import {
|
||||
UsePreferencesQueryResponse,
|
||||
ThreadViewPreferences,
|
||||
} from '#/state/queries/preferences/types'
|
||||
|
||||
export const DEFAULT_HOME_FEED_PREFS: UsePreferencesQueryResponse['feedViewPrefs'] =
|
||||
{
|
||||
hideReplies: false,
|
||||
hideRepliesByUnfollowed: false,
|
||||
hideRepliesByLikeCount: 0,
|
||||
hideReposts: false,
|
||||
hideQuotePosts: false,
|
||||
lab_mergeFeedEnabled: false, // experimental
|
||||
}
|
||||
|
||||
export const DEFAULT_THREAD_VIEW_PREFS: ThreadViewPreferences = {
|
||||
sort: 'newest',
|
||||
prioritizeFollowedUsers: true,
|
||||
lab_treeViewEnabled: false,
|
||||
}
|
||||
|
||||
const DEFAULT_PROD_FEED_PREFIX = (rkey: string) =>
|
||||
`at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/${rkey}`
|
||||
export const DEFAULT_PROD_FEEDS = {
|
||||
pinned: [DEFAULT_PROD_FEED_PREFIX('whats-hot')],
|
||||
saved: [DEFAULT_PROD_FEED_PREFIX('whats-hot')],
|
||||
}
|
257
src/state/queries/preferences/index.ts
Normal file
257
src/state/queries/preferences/index.ts
Normal file
|
@ -0,0 +1,257 @@
|
|||
import {useQuery, useMutation, useQueryClient} from '@tanstack/react-query'
|
||||
import {LabelPreference, BskyFeedViewPreference} from '@atproto/api'
|
||||
|
||||
import {track} from '#/lib/analytics/analytics'
|
||||
import {getAge} from '#/lib/strings/time'
|
||||
import {useSession} from '#/state/session'
|
||||
import {DEFAULT_LABEL_PREFERENCES} from '#/state/queries/preferences/moderation'
|
||||
import {
|
||||
ConfigurableLabelGroup,
|
||||
UsePreferencesQueryResponse,
|
||||
ThreadViewPreferences,
|
||||
} from '#/state/queries/preferences/types'
|
||||
import {temp__migrateLabelPref} from '#/state/queries/preferences/util'
|
||||
import {
|
||||
DEFAULT_HOME_FEED_PREFS,
|
||||
DEFAULT_THREAD_VIEW_PREFS,
|
||||
} from '#/state/queries/preferences/const'
|
||||
|
||||
export * from '#/state/queries/preferences/types'
|
||||
export * from '#/state/queries/preferences/moderation'
|
||||
export * from '#/state/queries/preferences/const'
|
||||
|
||||
export const usePreferencesQueryKey = ['getPreferences']
|
||||
|
||||
export function usePreferencesQuery() {
|
||||
const {agent} = useSession()
|
||||
return useQuery({
|
||||
queryKey: usePreferencesQueryKey,
|
||||
queryFn: async () => {
|
||||
const res = await agent.getPreferences()
|
||||
const preferences: UsePreferencesQueryResponse = {
|
||||
...res,
|
||||
feeds: {
|
||||
saved: res.feeds?.saved || [],
|
||||
pinned: res.feeds?.pinned || [],
|
||||
unpinned:
|
||||
res.feeds.saved?.filter(f => {
|
||||
return !res.feeds.pinned?.includes(f)
|
||||
}) || [],
|
||||
},
|
||||
// labels are undefined until set by user
|
||||
contentLabels: {
|
||||
nsfw: temp__migrateLabelPref(
|
||||
res.contentLabels?.nsfw || DEFAULT_LABEL_PREFERENCES.nsfw,
|
||||
),
|
||||
nudity: temp__migrateLabelPref(
|
||||
res.contentLabels?.nudity || DEFAULT_LABEL_PREFERENCES.nudity,
|
||||
),
|
||||
suggestive: temp__migrateLabelPref(
|
||||
res.contentLabels?.suggestive ||
|
||||
DEFAULT_LABEL_PREFERENCES.suggestive,
|
||||
),
|
||||
gore: temp__migrateLabelPref(
|
||||
res.contentLabels?.gore || DEFAULT_LABEL_PREFERENCES.gore,
|
||||
),
|
||||
hate: temp__migrateLabelPref(
|
||||
res.contentLabels?.hate || DEFAULT_LABEL_PREFERENCES.hate,
|
||||
),
|
||||
spam: temp__migrateLabelPref(
|
||||
res.contentLabels?.spam || DEFAULT_LABEL_PREFERENCES.spam,
|
||||
),
|
||||
impersonation: temp__migrateLabelPref(
|
||||
res.contentLabels?.impersonation ||
|
||||
DEFAULT_LABEL_PREFERENCES.impersonation,
|
||||
),
|
||||
},
|
||||
feedViewPrefs: {
|
||||
...DEFAULT_HOME_FEED_PREFS,
|
||||
...(res.feedViewPrefs.home || {}),
|
||||
},
|
||||
threadViewPrefs: {
|
||||
...DEFAULT_THREAD_VIEW_PREFS,
|
||||
...(res.threadViewPrefs ?? {}),
|
||||
},
|
||||
userAge: res.birthDate ? getAge(res.birthDate) : undefined,
|
||||
}
|
||||
return preferences
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useClearPreferencesMutation() {
|
||||
const {agent} = useSession()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async () => {
|
||||
await agent.app.bsky.actor.putPreferences({preferences: []})
|
||||
// triggers a refetch
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: usePreferencesQueryKey,
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function usePreferencesSetContentLabelMutation() {
|
||||
const {agent} = useSession()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation<
|
||||
void,
|
||||
unknown,
|
||||
{labelGroup: ConfigurableLabelGroup; visibility: LabelPreference}
|
||||
>({
|
||||
mutationFn: async ({labelGroup, visibility}) => {
|
||||
await agent.setContentLabelPref(labelGroup, visibility)
|
||||
// triggers a refetch
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: usePreferencesQueryKey,
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function usePreferencesSetAdultContentMutation() {
|
||||
const {agent} = useSession()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation<void, unknown, {enabled: boolean}>({
|
||||
mutationFn: async ({enabled}) => {
|
||||
await agent.setAdultContentEnabled(enabled)
|
||||
// triggers a refetch
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: usePreferencesQueryKey,
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function usePreferencesSetBirthDateMutation() {
|
||||
const {agent} = useSession()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation<void, unknown, {birthDate: Date}>({
|
||||
mutationFn: async ({birthDate}: {birthDate: Date}) => {
|
||||
await agent.setPersonalDetails({birthDate})
|
||||
// triggers a refetch
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: usePreferencesQueryKey,
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useSetFeedViewPreferencesMutation() {
|
||||
const {agent} = useSession()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation<void, unknown, Partial<BskyFeedViewPreference>>({
|
||||
mutationFn: async prefs => {
|
||||
await agent.setFeedViewPrefs('home', prefs)
|
||||
// triggers a refetch
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: usePreferencesQueryKey,
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useSetThreadViewPreferencesMutation() {
|
||||
const {agent} = useSession()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation<void, unknown, Partial<ThreadViewPreferences>>({
|
||||
mutationFn: async prefs => {
|
||||
await agent.setThreadViewPrefs(prefs)
|
||||
// triggers a refetch
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: usePreferencesQueryKey,
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useSetSaveFeedsMutation() {
|
||||
const {agent} = useSession()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation<
|
||||
void,
|
||||
unknown,
|
||||
Pick<UsePreferencesQueryResponse['feeds'], 'saved' | 'pinned'>
|
||||
>({
|
||||
mutationFn: async ({saved, pinned}) => {
|
||||
await agent.setSavedFeeds(saved, pinned)
|
||||
// triggers a refetch
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: usePreferencesQueryKey,
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useSaveFeedMutation() {
|
||||
const {agent} = useSession()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation<void, unknown, {uri: string}>({
|
||||
mutationFn: async ({uri}) => {
|
||||
await agent.addSavedFeed(uri)
|
||||
track('CustomFeed:Save')
|
||||
// triggers a refetch
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: usePreferencesQueryKey,
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useRemoveFeedMutation() {
|
||||
const {agent} = useSession()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation<void, unknown, {uri: string}>({
|
||||
mutationFn: async ({uri}) => {
|
||||
await agent.removeSavedFeed(uri)
|
||||
track('CustomFeed:Unsave')
|
||||
// triggers a refetch
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: usePreferencesQueryKey,
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function usePinFeedMutation() {
|
||||
const {agent} = useSession()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation<void, unknown, {uri: string}>({
|
||||
mutationFn: async ({uri}) => {
|
||||
await agent.addPinnedFeed(uri)
|
||||
track('CustomFeed:Pin', {uri})
|
||||
// triggers a refetch
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: usePreferencesQueryKey,
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useUnpinFeedMutation() {
|
||||
const {agent} = useSession()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation<void, unknown, {uri: string}>({
|
||||
mutationFn: async ({uri}) => {
|
||||
await agent.removePinnedFeed(uri)
|
||||
track('CustomFeed:Unpin', {uri})
|
||||
// triggers a refetch
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: usePreferencesQueryKey,
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
163
src/state/queries/preferences/moderation.ts
Normal file
163
src/state/queries/preferences/moderation.ts
Normal file
|
@ -0,0 +1,163 @@
|
|||
import {
|
||||
LabelPreference,
|
||||
ComAtprotoLabelDefs,
|
||||
ModerationOpts,
|
||||
} from '@atproto/api'
|
||||
|
||||
import {
|
||||
LabelGroup,
|
||||
ConfigurableLabelGroup,
|
||||
UsePreferencesQueryResponse,
|
||||
} from '#/state/queries/preferences/types'
|
||||
|
||||
export type Label = ComAtprotoLabelDefs.Label
|
||||
|
||||
export type LabelGroupConfig = {
|
||||
id: LabelGroup
|
||||
title: string
|
||||
isAdultImagery?: boolean
|
||||
subtitle?: string
|
||||
warning: string
|
||||
values: string[]
|
||||
}
|
||||
|
||||
export const DEFAULT_LABEL_PREFERENCES: Record<
|
||||
ConfigurableLabelGroup,
|
||||
LabelPreference
|
||||
> = {
|
||||
nsfw: 'hide',
|
||||
nudity: 'warn',
|
||||
suggestive: 'warn',
|
||||
gore: 'warn',
|
||||
hate: 'hide',
|
||||
spam: 'hide',
|
||||
impersonation: 'hide',
|
||||
}
|
||||
|
||||
export const ILLEGAL_LABEL_GROUP: LabelGroupConfig = {
|
||||
id: 'illegal',
|
||||
title: 'Illegal Content',
|
||||
warning: 'Illegal Content',
|
||||
values: ['csam', 'dmca-violation', 'nudity-nonconsensual'],
|
||||
}
|
||||
|
||||
export const ALWAYS_FILTER_LABEL_GROUP: LabelGroupConfig = {
|
||||
id: 'always-filter',
|
||||
title: 'Content Warning',
|
||||
warning: 'Content Warning',
|
||||
values: ['!filter'],
|
||||
}
|
||||
|
||||
export const ALWAYS_WARN_LABEL_GROUP: LabelGroupConfig = {
|
||||
id: 'always-warn',
|
||||
title: 'Content Warning',
|
||||
warning: 'Content Warning',
|
||||
values: ['!warn', 'account-security'],
|
||||
}
|
||||
|
||||
export const UNKNOWN_LABEL_GROUP: LabelGroupConfig = {
|
||||
id: 'unknown',
|
||||
title: 'Unknown Label',
|
||||
warning: 'Content Warning',
|
||||
values: [],
|
||||
}
|
||||
|
||||
export const CONFIGURABLE_LABEL_GROUPS: Record<
|
||||
ConfigurableLabelGroup,
|
||||
LabelGroupConfig
|
||||
> = {
|
||||
nsfw: {
|
||||
id: 'nsfw',
|
||||
title: 'Explicit Sexual Images',
|
||||
subtitle: 'i.e. pornography',
|
||||
warning: 'Sexually Explicit',
|
||||
values: ['porn', 'nsfl'],
|
||||
isAdultImagery: true,
|
||||
},
|
||||
nudity: {
|
||||
id: 'nudity',
|
||||
title: 'Other Nudity',
|
||||
subtitle: 'Including non-sexual and artistic',
|
||||
warning: 'Nudity',
|
||||
values: ['nudity'],
|
||||
isAdultImagery: true,
|
||||
},
|
||||
suggestive: {
|
||||
id: 'suggestive',
|
||||
title: 'Sexually Suggestive',
|
||||
subtitle: 'Does not include nudity',
|
||||
warning: 'Sexually Suggestive',
|
||||
values: ['sexual'],
|
||||
isAdultImagery: true,
|
||||
},
|
||||
gore: {
|
||||
id: 'gore',
|
||||
title: 'Violent / Bloody',
|
||||
subtitle: 'Gore, self-harm, torture',
|
||||
warning: 'Violence',
|
||||
values: ['gore', 'self-harm', 'torture', 'nsfl', 'corpse'],
|
||||
isAdultImagery: true,
|
||||
},
|
||||
hate: {
|
||||
id: 'hate',
|
||||
title: 'Hate Group Iconography',
|
||||
subtitle: 'Images of terror groups, articles covering events, etc.',
|
||||
warning: 'Hate Groups',
|
||||
values: ['icon-kkk', 'icon-nazi', 'icon-intolerant', 'behavior-intolerant'],
|
||||
},
|
||||
spam: {
|
||||
id: 'spam',
|
||||
title: 'Spam',
|
||||
subtitle: 'Excessive unwanted interactions',
|
||||
warning: 'Spam',
|
||||
values: ['spam'],
|
||||
},
|
||||
impersonation: {
|
||||
id: 'impersonation',
|
||||
title: 'Impersonation',
|
||||
subtitle: 'Accounts falsely claiming to be people or orgs',
|
||||
warning: 'Impersonation',
|
||||
values: ['impersonation'],
|
||||
},
|
||||
}
|
||||
|
||||
export function getModerationOpts({
|
||||
userDid,
|
||||
preferences,
|
||||
}: {
|
||||
userDid: string
|
||||
preferences: UsePreferencesQueryResponse
|
||||
}): ModerationOpts {
|
||||
return {
|
||||
userDid: userDid,
|
||||
adultContentEnabled: preferences.adultContentEnabled,
|
||||
labels: {
|
||||
porn: preferences.contentLabels.nsfw,
|
||||
sexual: preferences.contentLabels.suggestive,
|
||||
nudity: preferences.contentLabels.nudity,
|
||||
nsfl: preferences.contentLabels.gore,
|
||||
corpse: preferences.contentLabels.gore,
|
||||
gore: preferences.contentLabels.gore,
|
||||
torture: preferences.contentLabels.gore,
|
||||
'self-harm': preferences.contentLabels.gore,
|
||||
'intolerant-race': preferences.contentLabels.hate,
|
||||
'intolerant-gender': preferences.contentLabels.hate,
|
||||
'intolerant-sexual-orientation': preferences.contentLabels.hate,
|
||||
'intolerant-religion': preferences.contentLabels.hate,
|
||||
intolerant: preferences.contentLabels.hate,
|
||||
'icon-intolerant': preferences.contentLabels.hate,
|
||||
spam: preferences.contentLabels.spam,
|
||||
impersonation: preferences.contentLabels.impersonation,
|
||||
scam: 'warn',
|
||||
},
|
||||
labelers: [
|
||||
{
|
||||
labeler: {
|
||||
did: '',
|
||||
displayName: 'Bluesky Social',
|
||||
},
|
||||
labels: {},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
46
src/state/queries/preferences/types.ts
Normal file
46
src/state/queries/preferences/types.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
import {
|
||||
BskyPreferences,
|
||||
LabelPreference,
|
||||
BskyThreadViewPreference,
|
||||
} from '@atproto/api'
|
||||
|
||||
export type ConfigurableLabelGroup =
|
||||
| 'nsfw'
|
||||
| 'nudity'
|
||||
| 'suggestive'
|
||||
| 'gore'
|
||||
| 'hate'
|
||||
| 'spam'
|
||||
| 'impersonation'
|
||||
export type LabelGroup =
|
||||
| ConfigurableLabelGroup
|
||||
| 'illegal'
|
||||
| 'always-filter'
|
||||
| 'always-warn'
|
||||
| 'unknown'
|
||||
|
||||
export type UsePreferencesQueryResponse = Omit<
|
||||
BskyPreferences,
|
||||
'contentLabels' | 'feedViewPrefs' | 'feeds'
|
||||
> & {
|
||||
/*
|
||||
* Content labels previously included 'show', which has been deprecated in
|
||||
* favor of 'ignore'. The API can return legacy data from the database, and
|
||||
* we clean up the data in `usePreferencesQuery`.
|
||||
*/
|
||||
contentLabels: Record<ConfigurableLabelGroup, LabelPreference>
|
||||
feedViewPrefs: BskyPreferences['feedViewPrefs']['home']
|
||||
/**
|
||||
* User thread-view prefs, including newer fields that may not be typed yet.
|
||||
*/
|
||||
threadViewPrefs: ThreadViewPreferences
|
||||
userAge: number | undefined
|
||||
feeds: Required<BskyPreferences['feeds']> & {
|
||||
unpinned: string[]
|
||||
}
|
||||
}
|
||||
|
||||
export type ThreadViewPreferences = Omit<BskyThreadViewPreference, 'sort'> & {
|
||||
sort: 'oldest' | 'newest' | 'most-likes' | 'random' | string
|
||||
lab_treeViewEnabled: boolean
|
||||
}
|
16
src/state/queries/preferences/util.ts
Normal file
16
src/state/queries/preferences/util.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import {LabelPreference} from '@atproto/api'
|
||||
|
||||
/**
|
||||
* Content labels previously included 'show', which has been deprecated in
|
||||
* favor of 'ignore'. The API can return legacy data from the database, and
|
||||
* we clean up the data in `usePreferencesQuery`.
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
export function temp__migrateLabelPref(
|
||||
pref: LabelPreference | 'show',
|
||||
): LabelPreference {
|
||||
// @ts-ignore
|
||||
if (pref === 'show') return 'ignore'
|
||||
return pref
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue