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 -1
View File
@@ -69,7 +69,7 @@
"icons:optimize": "svgo -f ./assets/icons" "icons:optimize": "svgo -f ./assets/icons"
}, },
"dependencies": { "dependencies": {
"@atproto/api": "^0.15.11", "@atproto/api": "^0.15.12",
"@bitdrift/react-native": "^0.6.8", "@bitdrift/react-native": "^0.6.8",
"@braintree/sanitize-url": "^6.0.2", "@braintree/sanitize-url": "^6.0.2",
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet", "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
@@ -2,21 +2,20 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {HITSLOP_10} from '#/lib/constants' import {HITSLOP_10} from '#/lib/constants'
import {type ThreadPreferences} from '#/state/queries/preferences/useThreadPreferences'
import {Button, ButtonIcon} from '#/components/Button' import {Button, ButtonIcon} from '#/components/Button'
import {SettingsSliderVertical_Stroke2_Corner0_Rounded as SettingsSlider} from '#/components/icons/SettingsSlider' import {SettingsSliderVertical_Stroke2_Corner0_Rounded as SettingsSlider} from '#/components/icons/SettingsSlider'
import * as Menu from '#/components/Menu' import * as Menu from '#/components/Menu'
export function HeaderDropdown({ export function HeaderDropdown({
sortReplies, sort,
treeViewEnabled, view,
setSortReplies, setSort,
setTreeViewEnabled, setView,
}: { }: Pick<
sortReplies: string ThreadPreferences,
treeViewEnabled: boolean 'sort' | 'setSort' | 'view' | 'setView'
setSortReplies: (newValue: string) => void >): React.ReactNode {
setTreeViewEnabled: (newValue: boolean) => void
}): React.ReactNode {
const {_} = useLingui() const {_} = useLingui()
return ( return (
<Menu.Root> <Menu.Root>
@@ -42,22 +41,22 @@ export function HeaderDropdown({
<Menu.Item <Menu.Item
label={_(msg`Linear`)} label={_(msg`Linear`)}
onPress={() => { onPress={() => {
setTreeViewEnabled(false) setView('linear')
}}> }}>
<Menu.ItemText> <Menu.ItemText>
<Trans>Linear</Trans> <Trans>Linear</Trans>
</Menu.ItemText> </Menu.ItemText>
<Menu.ItemRadio selected={!treeViewEnabled} /> <Menu.ItemRadio selected={view === 'linear'} />
</Menu.Item> </Menu.Item>
<Menu.Item <Menu.Item
label={_(msg`Threaded`)} label={_(msg`Threaded`)}
onPress={() => { onPress={() => {
setTreeViewEnabled(true) setView('tree')
}}> }}>
<Menu.ItemText> <Menu.ItemText>
<Trans>Threaded</Trans> <Trans>Threaded</Trans>
</Menu.ItemText> </Menu.ItemText>
<Menu.ItemRadio selected={treeViewEnabled} /> <Menu.ItemRadio selected={view === 'tree'} />
</Menu.Item> </Menu.Item>
</Menu.Group> </Menu.Group>
<Menu.Divider /> <Menu.Divider />
@@ -68,32 +67,32 @@ export function HeaderDropdown({
<Menu.Item <Menu.Item
label={_(msg`Top replies first`)} label={_(msg`Top replies first`)}
onPress={() => { onPress={() => {
setSortReplies('top') setSort('top')
}}> }}>
<Menu.ItemText> <Menu.ItemText>
<Trans>Top replies first</Trans> <Trans>Top replies first</Trans>
</Menu.ItemText> </Menu.ItemText>
<Menu.ItemRadio selected={sortReplies === 'top'} /> <Menu.ItemRadio selected={sort === 'top'} />
</Menu.Item> </Menu.Item>
<Menu.Item <Menu.Item
label={_(msg`Oldest replies first`)} label={_(msg`Oldest replies first`)}
onPress={() => { onPress={() => {
setSortReplies('oldest') setSort('oldest')
}}> }}>
<Menu.ItemText> <Menu.ItemText>
<Trans>Oldest replies first</Trans> <Trans>Oldest replies first</Trans>
</Menu.ItemText> </Menu.ItemText>
<Menu.ItemRadio selected={sortReplies === 'oldest'} /> <Menu.ItemRadio selected={sort === 'oldest'} />
</Menu.Item> </Menu.Item>
<Menu.Item <Menu.Item
label={_(msg`Newest replies first`)} label={_(msg`Newest replies first`)}
onPress={() => { onPress={() => {
setSortReplies('newest') setSort('newest')
}}> }}>
<Menu.ItemText> <Menu.ItemText>
<Trans>Newest replies first</Trans> <Trans>Newest replies first</Trans>
</Menu.ItemText> </Menu.ItemText>
<Menu.ItemRadio selected={sortReplies === 'newest'} /> <Menu.ItemRadio selected={sort === 'newest'} />
</Menu.Item> </Menu.Item>
</Menu.Group> </Menu.Group>
</Menu.Outer> </Menu.Outer>
+44 -58
View File
@@ -8,7 +8,6 @@ import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {ScrollProvider} from '#/lib/ScrollContext' import {ScrollProvider} from '#/lib/ScrollContext'
import {cleanError} from '#/lib/strings/errors' import {cleanError} from '#/lib/strings/errors'
import {isNative} from '#/platform/detection' import {isNative} from '#/platform/detection'
import {useThreadPreferences} from '#/state/queries/preferences/useThreadPreferences'
import {type ThreadItem, usePostThread} from '#/state/queries/usePostThread' import {type ThreadItem, usePostThread} from '#/state/queries/usePostThread'
import {type OnPostSuccessData} from '#/state/shell/composer' import {type OnPostSuccessData} from '#/state/shell/composer'
import {PostThreadComposePrompt} from '#/view/com/post-thread/PostThreadComposePrompt' import {PostThreadComposePrompt} from '#/view/com/post-thread/PostThreadComposePrompt'
@@ -40,45 +39,23 @@ export function Inner({uri}: {uri: string | undefined}) {
const initialNumToRender = useInitialNumToRender() const initialNumToRender = useInitialNumToRender()
const {height: windowHeight} = useWindowDimensions() const {height: windowHeight} = useWindowDimensions()
const { /*
isLoaded: isThreadPreferencesLoaded, * One query to rule them all
sortReplies, */
setSortReplies, const thread = usePostThread({anchor: uri})
prioritizeFollowedUsers,
treeViewEnabled,
setTreeViewEnabled,
} = useThreadPreferences()
const { const optimisticOnPostReply = (payload: OnPostSuccessData) => {
isFetching, if (payload) {
isPlaceholderData, const {replyToUri, posts} = payload
error,
data,
refetch,
insertReplies,
showHiddenReplies,
} = usePostThread({
enabled: isThreadPreferencesLoaded,
params: {
anchor: uri,
sort: sortReplies,
view: treeViewEnabled ? 'tree' : 'linear',
prioritizeFollowedUsers,
},
})
const optimisticOnPostReply = (data: OnPostSuccessData) => {
if (data) {
const {replyToUri, posts} = data
if (replyToUri && posts.length) { if (replyToUri && posts.length) {
insertReplies(replyToUri, posts) thread.actions.insertReplies(replyToUri, posts)
} }
} }
} }
const {openComposer} = useOpenComposer() const {openComposer} = useOpenComposer()
const onReplyToAnchor = () => { const onReplyToAnchor = () => {
const anchorPost = data?.items.find( const anchorPost = thread.data.items.find(
slice => slice.type === 'threadPost' && slice.ui.isAnchor, slice => slice.type === 'threadPost' && slice.ui.isAnchor,
) )
if (anchorPost?.type !== 'threadPost') { if (anchorPost?.type !== 'threadPost') {
@@ -159,28 +136,28 @@ export function Inner({uri}: {uri: string | undefined}) {
const hasExhaustedReplies = useRef(false) const hasExhaustedReplies = useRef(false)
const onStartReached = () => { const onStartReached = () => {
if (isFetching) return if (thread.state.isFetching) return
// limit to 100 // limit to 100
setMaxParentCount(n => Math.min(100, n + PARENT_CHUNK_SIZE)) setMaxParentCount(n => Math.min(100, n + PARENT_CHUNK_SIZE))
} }
const onEndReached = () => { const onEndReached = () => {
if (isFetching) return if (thread.state.isFetching) return
// prevent any state mutations if we know we're done // prevent any state mutations if we know we're done
if (hasExhaustedReplies.current) return if (hasExhaustedReplies.current) return
setMaxRepliesCount(prev => prev + REPLIES_CHUNK_SIZE) setMaxRepliesCount(prev => prev + REPLIES_CHUNK_SIZE)
} }
const items = useMemo(() => { const slices = useMemo(() => {
const results: ThreadItem[] = [] const results: ThreadItem[] = []
if (!data?.items) return results if (!thread.data.items.length) return results
let repliesCount = 0 let repliesCount = 0
let totalRepliesCount = 0 let totalRepliesCount = 0
for (let i = 0; i < data.items.length; i++) { for (let i = 0; i < thread.data.items.length; i++) {
const item = data.items[i] const item = thread.data.items[i]
if ('depth' in item) { if ('depth' in item) {
if (item.depth === 0) { if (item.depth === 0) {
@@ -190,7 +167,7 @@ export function Inner({uri}: {uri: string | undefined}) {
const start = i - 1 const start = i - 1
const limit = Math.max(0, start - maxParentCount) const limit = Math.max(0, start - maxParentCount)
for (let pi = start; pi >= limit; pi--) { for (let pi = start; pi >= limit; pi--) {
results.unshift(data.items[pi]) results.unshift(thread.data.items[pi])
} }
} }
} else if (item.depth > 0) { } else if (item.depth > 0) {
@@ -207,12 +184,15 @@ export function Inner({uri}: {uri: string | undefined}) {
} }
// TODO should really just count these during traversal, can remove isPlaceholder data after that // TODO should really just count these during traversal, can remove isPlaceholder data after that
if (maxRepliesCount > totalRepliesCount && !isPlaceholderData) { if (
maxRepliesCount > totalRepliesCount &&
!thread.state.isPlaceholderData
) {
hasExhaustedReplies.current = true hasExhaustedReplies.current = true
} }
return results return results
}, [data, deferParents, maxParentCount, maxRepliesCount, isPlaceholderData]) }, [thread, deferParents, maxParentCount, maxRepliesCount])
const renderItem = ({item, index}: {item: ThreadItem; index: number}) => { const renderItem = ({item, index}: {item: ThreadItem; index: number}) => {
if (item.type === 'threadPost') { if (item.type === 'threadPost') {
@@ -221,7 +201,7 @@ export function Inner({uri}: {uri: string | undefined}) {
return ( return (
<ThreadPost <ThreadPost
item={item} item={item}
threadgateRecord={data?.threadgate?.record ?? undefined} threadgateRecord={thread.data.threadgate?.record ?? undefined}
overrides={{ overrides={{
topBorder: index === 0, // && !item.isParentLoading, // TODO topBorder: index === 0, // && !item.isParentLoading, // TODO
}} }}
@@ -235,19 +215,19 @@ export function Inner({uri}: {uri: string | undefined}) {
onLayout={deferParents ? () => setDeferParents(false) : undefined}> onLayout={deferParents ? () => setDeferParents(false) : undefined}>
<ThreadAnchor <ThreadAnchor
item={item} item={item}
threadgateRecord={data?.threadgate?.record ?? undefined} threadgateRecord={thread.data.threadgate?.record ?? undefined}
onPostSuccess={optimisticOnPostReply} onPostSuccess={optimisticOnPostReply}
/> />
</View> </View>
) )
} else { } else {
if (treeViewEnabled) { if (thread.state.view === 'tree') {
return ( return (
<ThreadReply <ThreadReply
item={item} item={item}
threadgateRecord={data?.threadgate?.record ?? undefined} threadgateRecord={thread.data.threadgate?.record ?? undefined}
overrides={{ overrides={{
moderation: showHiddenReplies && item.depth > 0, moderation: thread.state.hiddenRepliesVisible && item.depth > 0,
}} }}
onPostSuccess={optimisticOnPostReply} onPostSuccess={optimisticOnPostReply}
/> />
@@ -256,9 +236,9 @@ export function Inner({uri}: {uri: string | undefined}) {
return ( return (
<ThreadPost <ThreadPost
item={item} item={item}
threadgateRecord={data?.threadgate?.record ?? undefined} threadgateRecord={thread.data.threadgate?.record ?? undefined}
overrides={{ overrides={{
moderation: showHiddenReplies && item.depth > 0, moderation: thread.state.hiddenRepliesVisible && item.depth > 0,
}} }}
onPostSuccess={optimisticOnPostReply} onPostSuccess={optimisticOnPostReply}
/> />
@@ -266,7 +246,12 @@ export function Inner({uri}: {uri: string | undefined}) {
} }
} }
} else if (item.type === 'readMore') { } else if (item.type === 'readMore') {
return <ReadMore item={item} view={treeViewEnabled ? 'tree' : 'linear'} /> return (
<ReadMore
item={item}
view={thread.state.view === 'tree' ? 'tree' : 'linear'}
/>
)
} else if (item.type === 'threadPostBlocked') { } else if (item.type === 'threadPostBlocked') {
return ( return (
<View <View
@@ -340,23 +325,24 @@ export function Inner({uri}: {uri: string | undefined}) {
</Layout.Header.Content> </Layout.Header.Content>
<Layout.Header.Slot> <Layout.Header.Slot>
<HeaderDropdown <HeaderDropdown
sortReplies={sortReplies} sort={thread.state.sort}
treeViewEnabled={treeViewEnabled} setSort={thread.actions.setSort}
setSortReplies={setSortReplies} view={thread.state.view}
setTreeViewEnabled={setTreeViewEnabled} setView={thread.actions.setView}
/> />
</Layout.Header.Slot> </Layout.Header.Slot>
</Layout.Header.Outer> </Layout.Header.Outer>
{error ? ( {thread.state.error ? (
<PostThreadError error={error} /> <PostThreadError error={thread.state.error} />
) : ( ) : (
<ScrollProvider <ScrollProvider
// TODO do we need?
//onMomentumEnd={onMomentumEnd} //onMomentumEnd={onMomentumEnd}
> >
<List <List
ref={listRef} ref={listRef}
data={items} data={slices}
renderItem={renderItem} renderItem={renderItem}
keyExtractor={keyExtractor} keyExtractor={keyExtractor}
onContentSizeChange={onContentSizeChangeWebOnly} onContentSizeChange={onContentSizeChangeWebOnly}
@@ -379,8 +365,8 @@ export function Inner({uri}: {uri: string | undefined}) {
* purpose here so we get the loader on initial render * purpose here so we get the loader on initial render
*/ */
// isFetchingNextPage={isFetching} // isFetchingNextPage={isFetching}
error={cleanError(error)} error={cleanError(thread.state.error)}
onRetry={refetch} onRetry={thread.actions.refetch}
/* /*
* 200 is based on the minimum height of a post. This is enough * 200 is based on the minimum height of a post. This is enough
* extra height for the `maintainVisPos` to work without * extra height for the `maintainVisPos` to work without
@@ -1,23 +1,42 @@
import {useMemo, useState} from 'react' import {useMemo, useState} from 'react'
import {type AppBskyUnspeccedGetPostThreadV2} from '@atproto/api'
import {usePreferencesQuery} from '#/state/queries/preferences' 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 {data: preferences} = usePreferencesQuery()
const nextThreadPreferences = preferences?.threadViewPrefs const nextThreadPreferences = preferences?.threadViewPrefs
/* /*
* Create local state representations of server state * Create local state representations of server state
*/ */
const [sortReplies, setSortReplies] = useState( const [sort, setSort] = useState(
nextThreadPreferences?.sort ?? 'hotness', migrateFromSortV1(nextThreadPreferences?.sort || 'top'),
)
const [view, setView] = useState(
computeView({
treeViewEnabled: !!nextThreadPreferences?.lab_treeViewEnabled,
}),
) )
const [prioritizeFollowedUsers, setPrioritizeFollowedUsers] = useState( const [prioritizeFollowedUsers, setPrioritizeFollowedUsers] = useState(
!!nextThreadPreferences?.prioritizeFollowedUsers, !!nextThreadPreferences?.prioritizeFollowedUsers,
) )
const [treeViewEnabled, setTreeViewEnabled] = useState(
!!nextThreadPreferences?.lab_treeViewEnabled,
)
/** /**
* Cache existing and if we get a server update, reset local state * Cache existing and if we get a server update, reset local state
@@ -29,9 +48,13 @@ export function useThreadPreferences() {
/* /*
* Reset * Reset
*/ */
setSortReplies(nextThreadPreferences.sort) setSort(migrateFromSortV1(nextThreadPreferences.sort))
setPrioritizeFollowedUsers(nextThreadPreferences.prioritizeFollowedUsers) setPrioritizeFollowedUsers(nextThreadPreferences.prioritizeFollowedUsers)
setTreeViewEnabled(!!nextThreadPreferences.lab_treeViewEnabled) setView(
computeView({
treeViewEnabled: !!nextThreadPreferences.lab_treeViewEnabled,
}),
)
} }
const isLoaded = !!prevServerPrefs const isLoaded = !!prevServerPrefs
@@ -39,21 +62,46 @@ export function useThreadPreferences() {
return useMemo( return useMemo(
() => ({ () => ({
isLoaded, isLoaded,
sortReplies, sort,
setSortReplies, setSort,
prioritizeFollowedUsers, prioritizeFollowedUsers,
setPrioritizeFollowedUsers, setPrioritizeFollowedUsers,
treeViewEnabled, view,
setTreeViewEnabled, setView,
}), }),
[ [
isLoaded, isLoaded,
sortReplies, sort,
setSortReplies, setSort,
prioritizeFollowedUsers, prioritizeFollowedUsers,
setPrioritizeFollowedUsers, setPrioritizeFollowedUsers,
treeViewEnabled, view,
setTreeViewEnabled, 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 {wait} from '#/lib/async/wait'
import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useThreadPreferences} from '#/state/queries/preferences/useThreadPreferences'
import { import {
createCacheMutator, createCacheMutator,
getThreadPlaceholder, getThreadPlaceholder,
@@ -12,7 +13,6 @@ import {
createPostThreadHiddenQueryKey, createPostThreadHiddenQueryKey,
createPostThreadQueryKey, createPostThreadQueryKey,
type ThreadItem, type ThreadItem,
type UsePostThreadProps,
} from '#/state/queries/usePostThread/types' } from '#/state/queries/usePostThread/types'
import {getThreadgateRecord} from '#/state/queries/usePostThread/utils' import {getThreadgateRecord} from '#/state/queries/usePostThread/utils'
import {useAgent, useSession} from '#/state/session' import {useAgent, useSession} from '#/state/session'
@@ -20,34 +20,40 @@ import {useMergeThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies
export * from '#/state/queries/usePostThread/types' export * from '#/state/queries/usePostThread/types'
export function usePostThread({ export function usePostThread({anchor}: {anchor?: string}) {
enabled: isEnabled,
params,
}: UsePostThreadProps) {
const qc = useQueryClient() const qc = useQueryClient()
const agent = useAgent() const agent = useAgent()
const {hasSession} = useSession() const {hasSession} = useSession()
const moderationOpts = useModerationOpts() const moderationOpts = useModerationOpts()
const mergeThreadgateHiddenReplies = useMergeThreadgateHiddenReplies() const mergeThreadgateHiddenReplies = useMergeThreadgateHiddenReplies()
const {
const enabled = isEnabled !== false && !!params.anchor && !!moderationOpts isLoaded: isThreadPreferencesLoaded,
const queryKey = createPostThreadQueryKey({ sort,
params, setSort,
view,
setView,
prioritizeFollowedUsers,
} = useThreadPreferences()
const postThreadQueryKey = createPostThreadQueryKey({
anchor,
sort,
view,
prioritizeFollowedUsers,
}) })
const query = useQuery({ const query = useQuery({
enabled, enabled: isThreadPreferencesLoaded && !!anchor && !!moderationOpts,
queryKey, queryKey: postThreadQueryKey,
// gcTime: 0, // TODO faster if we let it cache // gcTime: 0, // TODO faster if we let it cache
async queryFn(ctx) { async queryFn(ctx) {
const {data} = await wait( const {data} = await wait(
400, 400,
agent.app.bsky.unspecced.getPostThreadV2({ agent.app.bsky.unspecced.getPostThreadV2({
anchor: params.anchor!, anchor: anchor!,
branchingFactor: params.view === 'linear' ? 1 : undefined, branchingFactor: view === 'linear' ? 1 : undefined,
below: 4, below: 4,
sort: params.sort, sort: sort,
prioritizeFollowedUsers: params.prioritizeFollowedUsers, prioritizeFollowedUsers: prioritizeFollowedUsers,
}), }),
) )
@@ -72,8 +78,8 @@ export function usePostThread({
} }
}, },
placeholderData() { placeholderData() {
if (!params.anchor) return if (!anchor) return
const placeholder = getThreadPlaceholder(qc, params.anchor) const placeholder = getThreadPlaceholder(qc, anchor)
/* /*
* Always return something here, even empty data, so that * Always return something here, even empty data, so that
* `isPlaceholderData` is always true, which we'll use to insert * `isPlaceholderData` is always true, which we'll use to insert
@@ -95,8 +101,8 @@ export function usePostThread({
}) })
const hasHiddenReplies = !!query.data?.hasHiddenReplies const hasHiddenReplies = !!query.data?.hasHiddenReplies
const [showHiddenReplies, setShowHiddenReplies] = useState(false) const [hiddenRepliesVisible, setHiddenRepliesVisible] = useState(false)
const [hiddenReplies, setHiddenReplies] = useState<ThreadItem[]>([]) const [hiddenItems, setHiddenItems] = useState<ThreadItem[]>([])
/** /**
* Loads hidden replies for this thread. Any replies that are moderated from * Loads hidden replies for this thread. Any replies that are moderated from
@@ -105,19 +111,19 @@ export function usePostThread({
*/ */
const loadHiddenReplies = useCallback(async () => { const loadHiddenReplies = useCallback(async () => {
// immediately show any moderated replies already in memory // immediately show any moderated replies already in memory
setShowHiddenReplies(true) setHiddenRepliesVisible(true)
// add skeletons for the replies that will be loaded // add skeletons for the replies that will be loaded
setHiddenReplies( setHiddenItems(
Array.from({length: 2}).map((_, i) => ({ Array.from({length: 2}).map((_, i) => ({
type: 'skeleton', type: 'skeleton',
key: `${params.anchor!}-reply-${i}`, key: `${anchor!}-reply-${i}`,
item: 'reply', item: 'reply',
})), })),
) )
const queryParams = { const queryParams = {
anchor: params.anchor!, anchor: anchor!,
prioritizeFollowedUsers: params.prioritizeFollowedUsers, prioritizeFollowedUsers: prioritizeFollowedUsers,
} }
const data = await wait( const data = await wait(
@@ -139,26 +145,28 @@ export function usePostThread({
), ),
moderationOpts: moderationOpts!, moderationOpts: moderationOpts!,
hasSession, hasSession,
view: params.view, view,
hasHiddenReplies, hasHiddenReplies,
showHiddenReplies, hiddenRepliesVisible,
skipHiddenReplyHandling: true, skipHiddenReplyHandling: true,
loadHiddenReplies, loadHiddenReplies,
}) })
// insert the hidden replies into the state // insert the hidden replies into the state
setHiddenReplies(items) setHiddenItems(items)
}, [ }, [
agent, agent,
params, view,
anchor,
prioritizeFollowedUsers,
hasSession, hasSession,
mergeThreadgateHiddenReplies, mergeThreadgateHiddenReplies,
moderationOpts, moderationOpts,
qc, qc,
query.data?.threadgate?.record, query.data?.threadgate?.record,
hasHiddenReplies, hasHiddenReplies,
showHiddenReplies, hiddenRepliesVisible,
setShowHiddenReplies, setHiddenRepliesVisible,
]) ])
const items = useMemo(() => { const items = useMemo(() => {
@@ -168,36 +176,36 @@ export function usePostThread({
), ),
moderationOpts: moderationOpts!, moderationOpts: moderationOpts!,
hasSession, hasSession,
view: params.view, view: view,
hasHiddenReplies, hasHiddenReplies,
showHiddenReplies, hiddenRepliesVisible,
loadHiddenReplies, loadHiddenReplies,
}) })
return results.concat(hiddenReplies) return results.concat(hiddenItems)
}, [ }, [
query.data, query.data,
mergeThreadgateHiddenReplies, mergeThreadgateHiddenReplies,
moderationOpts, moderationOpts,
hasSession, hasSession,
params.view, view,
hasHiddenReplies, hasHiddenReplies,
showHiddenReplies, hiddenRepliesVisible,
loadHiddenReplies, loadHiddenReplies,
hiddenReplies, hiddenItems,
]) ])
if (query.isPlaceholderData) { if (query.isPlaceholderData) {
const anchor = items.at(0) const anchorPost = items.at(0)
const skeletonReplies = const skeletonReplies =
anchor && anchor.type === 'threadPost' anchorPost && anchorPost.type === 'threadPost'
? anchor?.value.post.replyCount ?? 4 ? anchorPost?.value.post.replyCount ?? 4
: 4 : 4
if (!items.length) { if (!items.length) {
items.push({ items.push({
type: 'skeleton', type: 'skeleton',
key: params.anchor!, key: anchor!,
item: 'anchor', item: 'anchor',
}) })
@@ -213,7 +221,7 @@ export function usePostThread({
for (let i = 0; i < skeletonReplies; i++) { for (let i = 0; i < skeletonReplies; i++) {
items.push({ items.push({
type: 'skeleton', type: 'skeleton',
key: `${params.anchor!}-reply-${i}`, key: `${anchor!}-reply-${i}`,
item: 'reply', item: 'reply',
}) })
} }
@@ -222,23 +230,46 @@ export function usePostThread({
const mutator = useMemo( const mutator = useMemo(
() => () =>
createCacheMutator({ createCacheMutator({
params, params: {
queryKey, sort,
view,
},
queryKey: postThreadQueryKey,
queryClient: qc, queryClient: qc,
}), }),
[qc, params, queryKey], [qc, sort, view, postThreadQueryKey],
) )
return useMemo( return useMemo(
() => ({ () => ({
...query, state: {
isFetching: query.isFetching,
isPlaceholderData: query.isPlaceholderData,
error: query.error,
hiddenRepliesVisible,
sort,
view,
},
data: { data: {
items, items: items || [],
threadgate: query.data?.threadgate, threadgate: query.data?.threadgate,
}, },
showHiddenReplies, actions: {
insertReplies: mutator.insertReplies, 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' import {embedViewRecordToPostView} from '#/state/queries/util'
export function createCacheMutator({ export function createCacheMutator({
params,
queryKey,
queryClient, queryClient,
queryKey,
params,
}: { }: {
params: PostThreadParams
queryKey: ReturnType<typeof createPostThreadQueryKey>
queryClient: QueryClient queryClient: QueryClient
queryKey: ReturnType<typeof createPostThreadQueryKey>
// TODO could clean this up?
params: PostThreadParams
}) { }) {
return { return {
insertReplies( insertReplies(
+3 -3
View File
@@ -26,7 +26,7 @@ export function traverse(
hasSession, hasSession,
view, view,
hasHiddenReplies, hasHiddenReplies,
showHiddenReplies, hiddenRepliesVisible,
skipHiddenReplyHandling, skipHiddenReplyHandling,
loadHiddenReplies, loadHiddenReplies,
}: { }: {
@@ -35,7 +35,7 @@ export function traverse(
hasSession: boolean hasSession: boolean
view: PostThreadParams['view'] view: PostThreadParams['view']
hasHiddenReplies: boolean hasHiddenReplies: boolean
showHiddenReplies: boolean hiddenRepliesVisible: boolean
skipHiddenReplyHandling?: boolean skipHiddenReplyHandling?: boolean
loadHiddenReplies: () => Promise<void> loadHiddenReplies: () => Promise<void>
}, },
@@ -241,7 +241,7 @@ export function traverse(
if (!skipHiddenReplyHandling) { if (!skipHiddenReplyHandling) {
if (hidden.length || hasHiddenReplies) { if (hidden.length || hasHiddenReplies) {
if (showHiddenReplies) { if (hiddenRepliesVisible) {
items.push(...hidden) items.push(...hidden)
} else { } else {
items.push({ items.push({
+2 -8
View File
@@ -14,9 +14,8 @@ export type ApiThreadItem =
export const postThreadQueryKeyRoot = 'getPostThreadV2' as const export const postThreadQueryKeyRoot = 'getPostThreadV2' as const
export const postThreadHiddenQueryKeyRoot = 'getPostThreadHiddenV2' as const export const postThreadHiddenQueryKeyRoot = 'getPostThreadHiddenV2' as const
export const createPostThreadQueryKey = ( export const createPostThreadQueryKey = (props: PostThreadParams) =>
props: Pick<UsePostThreadProps, 'params'>, [postThreadQueryKeyRoot, props] as const
) => [postThreadQueryKeyRoot, props] as const
export const createPostThreadHiddenQueryKey = ( export const createPostThreadHiddenQueryKey = (
props: AppBskyUnspeccedGetPostThreadHiddenV2.QueryParams, props: AppBskyUnspeccedGetPostThreadHiddenV2.QueryParams,
@@ -30,11 +29,6 @@ export type PostThreadParams = Pick<
view: 'tree' | 'linear' view: 'tree' | 'linear'
} }
export type UsePostThreadProps = {
enabled?: boolean
params: PostThreadParams
}
export type ThreadItem = export type ThreadItem =
| { | {
type: 'threadPost' type: 'threadPost'
+5
View File
@@ -0,0 +1,5 @@
export type Literal<T, A = string> = T extends A
? string extends T
? never
: T
: never
+4 -4
View File
@@ -63,10 +63,10 @@
"@atproto/xrpc" "^0.7.0" "@atproto/xrpc" "^0.7.0"
"@atproto/xrpc-server" "^0.7.18" "@atproto/xrpc-server" "^0.7.18"
"@atproto/api@^0.15.11": "@atproto/api@^0.15.12":
version "0.15.11" version "0.15.12"
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.15.11.tgz#f3f0718216c00bc405d35e0ce89ad30cedb2fb30" resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.15.12.tgz#0b86eef80b052194353926327acf85c6299b9c04"
integrity sha512-+XNOIqNPa1BWXzoi0mw6Qmx6kYlQPo60bhSrlxdhRYYH9CIgFAGmXrtb+MuAJoKgtSKX/2CBPihDsKEKEj8mfw== integrity sha512-51IHenZMA+Ekfe2OlZL/mTFqvZQU93jI4xsLvTFhGc4tSQYCHV9r/AJTANPZLFrhm9GfWZ0n90r/9IQl9eicjg==
dependencies: dependencies:
"@atproto/common-web" "^0.4.2" "@atproto/common-web" "^0.4.2"
"@atproto/lexicon" "^0.4.11" "@atproto/lexicon" "^0.4.11"