Checkpoint move state into meta query

This commit is contained in:
Eric Bailey
2025-06-04 10:57:38 -05:00
parent 11001172b3
commit ce776fd050
10 changed files with 227 additions and 163 deletions
@@ -1,23 +1,42 @@
import {useMemo, useState} from 'react'
import {type AppBskyUnspeccedGetPostThreadV2} from '@atproto/api'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {type Literal} from '#/types/utils'
export function useThreadPreferences() {
export type ThreadSortOption = Literal<
AppBskyUnspeccedGetPostThreadV2.QueryParams['sort'],
string
>
export type ThreadViewOption = 'linear' | 'tree'
export type ThreadPreferences = {
isLoaded: boolean
sort: ThreadSortOption
setSort: (sort: ThreadSortOption) => void
view: ThreadViewOption
setView: (view: ThreadViewOption) => void
prioritizeFollowedUsers: boolean
setPrioritizeFollowedUsers: (prioritize: boolean) => void
}
export function useThreadPreferences(): ThreadPreferences {
const {data: preferences} = usePreferencesQuery()
const nextThreadPreferences = preferences?.threadViewPrefs
/*
* Create local state representations of server state
*/
const [sortReplies, setSortReplies] = useState(
nextThreadPreferences?.sort ?? 'hotness',
const [sort, setSort] = useState(
migrateFromSortV1(nextThreadPreferences?.sort || 'top'),
)
const [view, setView] = useState(
computeView({
treeViewEnabled: !!nextThreadPreferences?.lab_treeViewEnabled,
}),
)
const [prioritizeFollowedUsers, setPrioritizeFollowedUsers] = useState(
!!nextThreadPreferences?.prioritizeFollowedUsers,
)
const [treeViewEnabled, setTreeViewEnabled] = useState(
!!nextThreadPreferences?.lab_treeViewEnabled,
)
/**
* Cache existing and if we get a server update, reset local state
@@ -29,9 +48,13 @@ export function useThreadPreferences() {
/*
* Reset
*/
setSortReplies(nextThreadPreferences.sort)
setSort(migrateFromSortV1(nextThreadPreferences.sort))
setPrioritizeFollowedUsers(nextThreadPreferences.prioritizeFollowedUsers)
setTreeViewEnabled(!!nextThreadPreferences.lab_treeViewEnabled)
setView(
computeView({
treeViewEnabled: !!nextThreadPreferences.lab_treeViewEnabled,
}),
)
}
const isLoaded = !!prevServerPrefs
@@ -39,21 +62,46 @@ export function useThreadPreferences() {
return useMemo(
() => ({
isLoaded,
sortReplies,
setSortReplies,
sort,
setSort,
prioritizeFollowedUsers,
setPrioritizeFollowedUsers,
treeViewEnabled,
setTreeViewEnabled,
view,
setView,
}),
[
isLoaded,
sortReplies,
setSortReplies,
sort,
setSort,
prioritizeFollowedUsers,
setPrioritizeFollowedUsers,
treeViewEnabled,
setTreeViewEnabled,
view,
setView,
],
)
}
/**
* Migrates user thread preferences from the old sort values to V2
*/
function migrateFromSortV1(sort: string): ThreadSortOption {
switch (sort) {
case 'oldest':
return 'oldest'
case 'newest':
return 'newest'
default:
return 'top'
}
}
/**
* Transforms existing treeViewEnabled preference into a ThreadViewOption
*/
function computeView({
treeViewEnabled,
}: {
treeViewEnabled: boolean
}): ThreadViewOption {
return treeViewEnabled ? 'tree' : 'linear'
}
+80 -49
View File
@@ -3,6 +3,7 @@ import {useQuery, useQueryClient} from '@tanstack/react-query'
import {wait} from '#/lib/async/wait'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useThreadPreferences} from '#/state/queries/preferences/useThreadPreferences'
import {
createCacheMutator,
getThreadPlaceholder,
@@ -12,7 +13,6 @@ import {
createPostThreadHiddenQueryKey,
createPostThreadQueryKey,
type ThreadItem,
type UsePostThreadProps,
} from '#/state/queries/usePostThread/types'
import {getThreadgateRecord} from '#/state/queries/usePostThread/utils'
import {useAgent, useSession} from '#/state/session'
@@ -20,34 +20,40 @@ import {useMergeThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies
export * from '#/state/queries/usePostThread/types'
export function usePostThread({
enabled: isEnabled,
params,
}: UsePostThreadProps) {
export function usePostThread({anchor}: {anchor?: string}) {
const qc = useQueryClient()
const agent = useAgent()
const {hasSession} = useSession()
const moderationOpts = useModerationOpts()
const mergeThreadgateHiddenReplies = useMergeThreadgateHiddenReplies()
const enabled = isEnabled !== false && !!params.anchor && !!moderationOpts
const queryKey = createPostThreadQueryKey({
params,
const {
isLoaded: isThreadPreferencesLoaded,
sort,
setSort,
view,
setView,
prioritizeFollowedUsers,
} = useThreadPreferences()
const postThreadQueryKey = createPostThreadQueryKey({
anchor,
sort,
view,
prioritizeFollowedUsers,
})
const query = useQuery({
enabled,
queryKey,
enabled: isThreadPreferencesLoaded && !!anchor && !!moderationOpts,
queryKey: postThreadQueryKey,
// gcTime: 0, // TODO faster if we let it cache
async queryFn(ctx) {
const {data} = await wait(
400,
agent.app.bsky.unspecced.getPostThreadV2({
anchor: params.anchor!,
branchingFactor: params.view === 'linear' ? 1 : undefined,
anchor: anchor!,
branchingFactor: view === 'linear' ? 1 : undefined,
below: 4,
sort: params.sort,
prioritizeFollowedUsers: params.prioritizeFollowedUsers,
sort: sort,
prioritizeFollowedUsers: prioritizeFollowedUsers,
}),
)
@@ -72,8 +78,8 @@ export function usePostThread({
}
},
placeholderData() {
if (!params.anchor) return
const placeholder = getThreadPlaceholder(qc, params.anchor)
if (!anchor) return
const placeholder = getThreadPlaceholder(qc, anchor)
/*
* Always return something here, even empty data, so that
* `isPlaceholderData` is always true, which we'll use to insert
@@ -95,8 +101,8 @@ export function usePostThread({
})
const hasHiddenReplies = !!query.data?.hasHiddenReplies
const [showHiddenReplies, setShowHiddenReplies] = useState(false)
const [hiddenReplies, setHiddenReplies] = useState<ThreadItem[]>([])
const [hiddenRepliesVisible, setHiddenRepliesVisible] = useState(false)
const [hiddenItems, setHiddenItems] = useState<ThreadItem[]>([])
/**
* Loads hidden replies for this thread. Any replies that are moderated from
@@ -105,19 +111,19 @@ export function usePostThread({
*/
const loadHiddenReplies = useCallback(async () => {
// immediately show any moderated replies already in memory
setShowHiddenReplies(true)
setHiddenRepliesVisible(true)
// add skeletons for the replies that will be loaded
setHiddenReplies(
setHiddenItems(
Array.from({length: 2}).map((_, i) => ({
type: 'skeleton',
key: `${params.anchor!}-reply-${i}`,
key: `${anchor!}-reply-${i}`,
item: 'reply',
})),
)
const queryParams = {
anchor: params.anchor!,
prioritizeFollowedUsers: params.prioritizeFollowedUsers,
anchor: anchor!,
prioritizeFollowedUsers: prioritizeFollowedUsers,
}
const data = await wait(
@@ -139,26 +145,28 @@ export function usePostThread({
),
moderationOpts: moderationOpts!,
hasSession,
view: params.view,
view,
hasHiddenReplies,
showHiddenReplies,
hiddenRepliesVisible,
skipHiddenReplyHandling: true,
loadHiddenReplies,
})
// insert the hidden replies into the state
setHiddenReplies(items)
setHiddenItems(items)
}, [
agent,
params,
view,
anchor,
prioritizeFollowedUsers,
hasSession,
mergeThreadgateHiddenReplies,
moderationOpts,
qc,
query.data?.threadgate?.record,
hasHiddenReplies,
showHiddenReplies,
setShowHiddenReplies,
hiddenRepliesVisible,
setHiddenRepliesVisible,
])
const items = useMemo(() => {
@@ -168,36 +176,36 @@ export function usePostThread({
),
moderationOpts: moderationOpts!,
hasSession,
view: params.view,
view: view,
hasHiddenReplies,
showHiddenReplies,
hiddenRepliesVisible,
loadHiddenReplies,
})
return results.concat(hiddenReplies)
return results.concat(hiddenItems)
}, [
query.data,
mergeThreadgateHiddenReplies,
moderationOpts,
hasSession,
params.view,
view,
hasHiddenReplies,
showHiddenReplies,
hiddenRepliesVisible,
loadHiddenReplies,
hiddenReplies,
hiddenItems,
])
if (query.isPlaceholderData) {
const anchor = items.at(0)
const anchorPost = items.at(0)
const skeletonReplies =
anchor && anchor.type === 'threadPost'
? anchor?.value.post.replyCount ?? 4
anchorPost && anchorPost.type === 'threadPost'
? anchorPost?.value.post.replyCount ?? 4
: 4
if (!items.length) {
items.push({
type: 'skeleton',
key: params.anchor!,
key: anchor!,
item: 'anchor',
})
@@ -213,7 +221,7 @@ export function usePostThread({
for (let i = 0; i < skeletonReplies; i++) {
items.push({
type: 'skeleton',
key: `${params.anchor!}-reply-${i}`,
key: `${anchor!}-reply-${i}`,
item: 'reply',
})
}
@@ -222,23 +230,46 @@ export function usePostThread({
const mutator = useMemo(
() =>
createCacheMutator({
params,
queryKey,
params: {
sort,
view,
},
queryKey: postThreadQueryKey,
queryClient: qc,
}),
[qc, params, queryKey],
[qc, sort, view, postThreadQueryKey],
)
return useMemo(
() => ({
...query,
state: {
isFetching: query.isFetching,
isPlaceholderData: query.isPlaceholderData,
error: query.error,
hiddenRepliesVisible,
sort,
view,
},
data: {
items,
items: items || [],
threadgate: query.data?.threadgate,
},
showHiddenReplies,
insertReplies: mutator.insertReplies,
actions: {
insertReplies: mutator.insertReplies,
refetch: query.refetch,
setSort,
setView,
},
}),
[query, items, mutator.insertReplies, showHiddenReplies],
[
query,
items,
mutator.insertReplies,
hiddenRepliesVisible,
sort,
view,
setSort,
setView,
],
)
}
@@ -24,13 +24,14 @@ import {didOrHandleUriMatches, getEmbeddedPost} from '#/state/queries/util'
import {embedViewRecordToPostView} from '#/state/queries/util'
export function createCacheMutator({
params,
queryKey,
queryClient,
queryKey,
params,
}: {
params: PostThreadParams
queryKey: ReturnType<typeof createPostThreadQueryKey>
queryClient: QueryClient
queryKey: ReturnType<typeof createPostThreadQueryKey>
// TODO could clean this up?
params: PostThreadParams
}) {
return {
insertReplies(
+3 -3
View File
@@ -26,7 +26,7 @@ export function traverse(
hasSession,
view,
hasHiddenReplies,
showHiddenReplies,
hiddenRepliesVisible,
skipHiddenReplyHandling,
loadHiddenReplies,
}: {
@@ -35,7 +35,7 @@ export function traverse(
hasSession: boolean
view: PostThreadParams['view']
hasHiddenReplies: boolean
showHiddenReplies: boolean
hiddenRepliesVisible: boolean
skipHiddenReplyHandling?: boolean
loadHiddenReplies: () => Promise<void>
},
@@ -241,7 +241,7 @@ export function traverse(
if (!skipHiddenReplyHandling) {
if (hidden.length || hasHiddenReplies) {
if (showHiddenReplies) {
if (hiddenRepliesVisible) {
items.push(...hidden)
} else {
items.push({
+2 -8
View File
@@ -14,9 +14,8 @@ export type ApiThreadItem =
export const postThreadQueryKeyRoot = 'getPostThreadV2' as const
export const postThreadHiddenQueryKeyRoot = 'getPostThreadHiddenV2' as const
export const createPostThreadQueryKey = (
props: Pick<UsePostThreadProps, 'params'>,
) => [postThreadQueryKeyRoot, props] as const
export const createPostThreadQueryKey = (props: PostThreadParams) =>
[postThreadQueryKeyRoot, props] as const
export const createPostThreadHiddenQueryKey = (
props: AppBskyUnspeccedGetPostThreadHiddenV2.QueryParams,
@@ -30,11 +29,6 @@ export type PostThreadParams = Pick<
view: 'tree' | 'linear'
}
export type UsePostThreadProps = {
enabled?: boolean
params: PostThreadParams
}
export type ThreadItem =
| {
type: 'threadPost'