Checkpoint move state into meta query
This commit is contained in:
+1
-1
@@ -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>
|
||||||
|
|||||||
@@ -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'
|
||||||
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export type Literal<T, A = string> = T extends A
|
||||||
|
? string extends T
|
||||||
|
? never
|
||||||
|
: T
|
||||||
|
: never
|
||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user