This commit is contained in:
Eric Bailey
2025-06-04 15:12:28 -05:00
parent fc4fd80392
commit 20a1fc4f03
4 changed files with 172 additions and 127 deletions
+2 -2
View File
@@ -227,7 +227,7 @@ export function Inner({uri}: {uri: string | undefined}) {
item={item} item={item}
threadgateRecord={thread.data.threadgate?.record ?? undefined} threadgateRecord={thread.data.threadgate?.record ?? undefined}
overrides={{ overrides={{
moderation: thread.state.hiddenRepliesVisible && item.depth > 0, moderation: thread.state.hiddenItemsVisible && item.depth > 0,
}} }}
onPostSuccess={optimisticOnPostReply} onPostSuccess={optimisticOnPostReply}
/> />
@@ -238,7 +238,7 @@ export function Inner({uri}: {uri: string | undefined}) {
item={item} item={item}
threadgateRecord={thread.data.threadgate?.record ?? undefined} threadgateRecord={thread.data.threadgate?.record ?? undefined}
overrides={{ overrides={{
moderation: thread.state.hiddenRepliesVisible && item.depth > 0, moderation: thread.state.hiddenItemsVisible && item.depth > 0,
}} }}
onPostSuccess={optimisticOnPostReply} onPostSuccess={optimisticOnPostReply}
/> />
+151 -100
View File
@@ -8,11 +8,12 @@ import {
createCacheMutator, createCacheMutator,
getThreadPlaceholder, getThreadPlaceholder,
} from '#/state/queries/usePostThread/queryCache' } from '#/state/queries/usePostThread/queryCache'
import {combine,traverse} from '#/state/queries/usePostThread/traversal' import {combine, traverse} from '#/state/queries/usePostThread/traversal'
import { import {
createPostThreadHiddenQueryKey, createPostThreadHiddenQueryKey,
createPostThreadQueryKey, createPostThreadQueryKey,
type ThreadItem, type ThreadItem,
type UsePostThreadQueryResult,
} 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 * as views from '#/state/queries/usePostThread/views' import * as views from '#/state/queries/usePostThread/views'
@@ -35,6 +36,7 @@ export function usePostThread({anchor}: {anchor?: string}) {
setView: baseSetView, setView: baseSetView,
prioritizeFollowedUsers, prioritizeFollowedUsers,
} = useThreadPreferences() } = useThreadPreferences()
const postThreadQueryKey = createPostThreadQueryKey({ const postThreadQueryKey = createPostThreadQueryKey({
anchor, anchor,
sort, sort,
@@ -42,7 +44,7 @@ export function usePostThread({anchor}: {anchor?: string}) {
prioritizeFollowedUsers, prioritizeFollowedUsers,
}) })
const query = useQuery({ const query = useQuery<UsePostThreadQueryResult>({
enabled: isThreadPreferencesLoaded && !!anchor && !!moderationOpts, enabled: isThreadPreferencesLoaded && !!anchor && !!moderationOpts,
queryKey: postThreadQueryKey, queryKey: postThreadQueryKey,
// gcTime: 0, // TODO faster if we let it cache // gcTime: 0, // TODO faster if we let it cache
@@ -73,10 +75,18 @@ export function usePostThread({anchor}: {anchor?: string}) {
ctx.meta.hasHiddenReplies = true ctx.meta.hasHiddenReplies = true
} }
return { const result = {
...data, thread: data.thread || [],
threadgate: data.threadgate,
hasHiddenReplies: !!ctx.meta.hasHiddenReplies, hasHiddenReplies: !!ctx.meta.hasHiddenReplies,
} }
const record = getThreadgateRecord(result.threadgate)
if (result.threadgate && record) {
result.threadgate.record = record
}
return result as UsePostThreadQueryResult
}, },
placeholderData() { placeholderData() {
if (!anchor) return if (!anchor) return
@@ -90,39 +100,86 @@ export function usePostThread({anchor}: {anchor?: string}) {
return {thread, threadgate: undefined, hasHiddenReplies: false} return {thread, threadgate: undefined, hasHiddenReplies: false}
}, },
select(data) { select(data) {
const threadgate = getThreadgateRecord(data.threadgate) const record = getThreadgateRecord(data.threadgate)
return { if (data.threadgate && record) {
...data, data.threadgate.record = record
threadgate: {
...data.threadgate,
record: threadgate,
},
} }
return data
}, },
}) })
const hasServerHiddenReplies = !!query.data?.hasHiddenReplies const thread = useMemo(() => query.data?.thread || [], [query.data?.thread])
const [hiddenRepliesVisible, setHiddenRepliesVisible] = useState(false) const threadgate = useMemo(
() => query.data?.threadgate,
[query.data?.threadgate],
)
const hasServerHiddenItems = useMemo(
() => !!query.data?.hasHiddenReplies,
[query.data?.hasHiddenReplies],
)
const [hiddenItemsVisible, setHiddenItemsVisible] = useState(false)
const [additionalHiddenItems, setAdditionalHiddenItems] = useState< const [additionalHiddenItems, setAdditionalHiddenItems] = useState<
ThreadItem[] ThreadItem[]
>([]) >([])
/**
* Sets the sort order for the thread and resets the hidden items
*/
const setSort: typeof baseSetSort = useCallback(
nextSort => {
setHiddenItemsVisible(false)
setAdditionalHiddenItems([])
baseSetSort(nextSort)
},
[baseSetSort, setAdditionalHiddenItems, setHiddenItemsVisible],
)
/**
* Sets the view variant for the thread and resets the hidden items
*/
const setView: typeof baseSetView = useCallback(
nextView => {
setHiddenItemsVisible(false)
setAdditionalHiddenItems([])
baseSetView(nextView)
},
[baseSetView, setAdditionalHiddenItems, setHiddenItemsVisible],
)
/**
* Creates a mutator for the post thread cache. This is used to insert
* replies into the thread cache after posting.
*/
const mutator = useMemo(
() =>
createCacheMutator({
params: {
sort,
view,
},
queryKey: postThreadQueryKey,
queryClient: qc,
}),
[qc, sort, view, postThreadQueryKey],
)
/** /**
* Loads hidden replies for this thread. Any replies that are moderated from * Loads hidden replies for this thread. Any replies that are moderated from
* the initial visible response(s) are shown immediately. Remote data is * the initial visible response(s) are shown immediately. Remote data is
* fetched and inserted when it's available. * fetched and inserted when it's available.
*/ */
const loadHiddenReplies = useCallback(async () => { const loadServerHiddenItems = useCallback(async () => {
/* /*
* Show any moderated replies already in memory that were handled here on * Show any moderated replies already in memory that were handled here on
* the client. If there are server-hidden replies, we'll fetch those next. * the client. If there are server-hidden replies, we'll fetch those next.
*/ */
setHiddenRepliesVisible(true) setHiddenItemsVisible(true)
/* /*
* If there are no server hidden replies, just stop here. * If there are no server hidden replies, just stop here.
*/ */
if (!hasServerHiddenReplies) return if (!hasServerHiddenItems) return
setAdditionalHiddenItems( setAdditionalHiddenItems(
Array.from({length: 2}).map((_, i) => Array.from({length: 2}).map((_, i) =>
@@ -153,9 +210,7 @@ export function usePostThread({anchor}: {anchor?: string}) {
const {items} = traverse(data || [], { const {items} = traverse(data || [], {
view, view,
skipHiddenReplyHandling: true, skipHiddenReplyHandling: true,
threadgateHiddenReplies: mergeThreadgateHiddenReplies( threadgateHiddenReplies: mergeThreadgateHiddenReplies(threadgate?.record),
query.data?.threadgate?.record,
),
moderationOpts: moderationOpts!, moderationOpts: moderationOpts!,
}) })
@@ -169,123 +224,118 @@ export function usePostThread({anchor}: {anchor?: string}) {
prioritizeFollowedUsers, prioritizeFollowedUsers,
mergeThreadgateHiddenReplies, mergeThreadgateHiddenReplies,
moderationOpts, moderationOpts,
query.data?.threadgate?.record, threadgate?.record,
hasServerHiddenReplies, hasServerHiddenItems,
setHiddenRepliesVisible, setHiddenItemsVisible,
]) ])
const combined = useMemo(() => { /**
const traversal = traverse(query.data?.thread || [], { * Builds the full set of thread items, minus any server-hidden replies.
*/
const threadItems = useMemo(() => {
const traversal = traverse(thread, {
view: view, view: view,
threadgateHiddenReplies: mergeThreadgateHiddenReplies( threadgateHiddenReplies: mergeThreadgateHiddenReplies(threadgate?.record),
query.data?.threadgate?.record,
),
moderationOpts: moderationOpts!, moderationOpts: moderationOpts!,
}) })
return combine(traversal, { return combine(traversal, {
hasSession, hasSession,
hasServerHiddenReplies, hasServerHiddenItems,
hiddenRepliesVisible, hiddenItemsVisible,
loadHiddenReplies, loadServerHiddenItems,
}) })
}, [ }, [
query.data, thread,
threadgate?.record,
mergeThreadgateHiddenReplies, mergeThreadgateHiddenReplies,
moderationOpts, moderationOpts,
hasSession, hasSession,
view, view,
hasServerHiddenReplies, hasServerHiddenItems,
hiddenRepliesVisible, hiddenItemsVisible,
loadHiddenReplies, loadServerHiddenItems,
]) ])
/**
* Computes the final thread items based on load state and the availability
* of server-hidden replies.
*/
const items = useMemo(() => { const items = useMemo(() => {
return combined.concat(additionalHiddenItems) if (query.isPlaceholderData) {
}, [combined, additionalHiddenItems]) const anchorPost = threadItems.at(0)
const skeletonReplies =
anchorPost && anchorPost.type === 'threadPost'
? anchorPost?.value.post.replyCount ?? 4
: 4
if (query.isPlaceholderData) { if (!threadItems.length) {
const anchorPost = items.at(0) threadItems.push(
const skeletonReplies =
anchorPost && anchorPost.type === 'threadPost'
? anchorPost?.value.post.replyCount ?? 4
: 4
if (!items.length) {
items.push(
views.skeleton({
key: anchor!,
item: 'anchor',
}),
)
if (hasSession) {
items.push(
views.skeleton({ views.skeleton({
key: 'replyComposer', key: anchor!,
item: 'replyComposer', item: 'anchor',
}),
)
if (hasSession) {
threadItems.push(
views.skeleton({
key: 'replyComposer',
item: 'replyComposer',
}),
)
}
}
for (let i = 0; i < skeletonReplies; i++) {
threadItems.push(
views.skeleton({
key: `${anchor!}-reply-${i}`,
item: 'reply',
}), }),
) )
} }
return threadItems
} else {
return threadItems.concat(additionalHiddenItems)
} }
}, [
for (let i = 0; i < skeletonReplies; i++) { query.isPlaceholderData,
items.push( threadItems,
views.skeleton({ additionalHiddenItems,
key: `${anchor!}-reply-${i}`, anchor,
item: 'reply', hasSession,
}), ])
)
}
}
const mutator = useMemo(
() =>
createCacheMutator({
params: {
sort,
view,
},
queryKey: postThreadQueryKey,
queryClient: qc,
}),
[qc, sort, view, postThreadQueryKey],
)
const setSort: typeof baseSetSort = useCallback(
nextSort => {
setHiddenRepliesVisible(false)
setAdditionalHiddenItems([])
baseSetSort(nextSort)
},
[baseSetSort, setAdditionalHiddenItems, setHiddenRepliesVisible],
)
const setView: typeof baseSetView = useCallback(
nextView => {
setHiddenRepliesVisible(false)
setAdditionalHiddenItems([])
baseSetView(nextView)
},
[baseSetView, setAdditionalHiddenItems, setHiddenRepliesVisible],
)
return useMemo( return useMemo(
() => ({ () => ({
state: { state: {
/*
* Copy in any query state that is useful
*/
isFetching: query.isFetching, isFetching: query.isFetching,
isPlaceholderData: query.isPlaceholderData, isPlaceholderData: query.isPlaceholderData,
error: query.error, error: query.error,
hiddenRepliesVisible, /*
* Other state
*/
sort, sort,
view, view,
hiddenItemsVisible,
}, },
data: { data: {
items: items || [], items,
threadgate: query.data?.threadgate, threadgate,
}, },
actions: { actions: {
/*
* Copy in any query actions that are useful
*/
insertReplies: mutator.insertReplies, insertReplies: mutator.insertReplies,
refetch: query.refetch, refetch: query.refetch,
/*
* Other actions
*/
setSort, setSort,
setView, setView,
}, },
@@ -294,11 +344,12 @@ export function usePostThread({anchor}: {anchor?: string}) {
query, query,
items, items,
mutator.insertReplies, mutator.insertReplies,
hiddenRepliesVisible, hiddenItemsVisible,
sort, sort,
view, view,
setSort, setSort,
setView, setView,
threadgate,
], ],
) )
} }
+10 -25
View File
@@ -1,8 +1,4 @@
import { import {AppBskyUnspeccedDefs, type ModerationOpts} from '@atproto/api'
AppBskyUnspeccedDefs,
type ModerationDecision,
type ModerationOpts,
} from '@atproto/api'
import { import {
type ApiThreadItem, type ApiThreadItem,
@@ -376,14 +372,14 @@ export function combine(
{items, hidden}: {items: ThreadItem[]; hidden: ThreadItem[]}, {items, hidden}: {items: ThreadItem[]; hidden: ThreadItem[]},
{ {
hasSession, hasSession,
hiddenRepliesVisible, hiddenItemsVisible,
hasServerHiddenReplies, hasServerHiddenItems,
loadHiddenReplies, loadServerHiddenItems,
}: { }: {
hasSession: boolean hasSession: boolean
hiddenRepliesVisible: boolean hiddenItemsVisible: boolean
hasServerHiddenReplies: boolean hasServerHiddenItems: boolean
loadHiddenReplies: () => Promise<void> loadServerHiddenItems: () => Promise<void>
}, },
) { ) {
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
@@ -402,14 +398,14 @@ export function combine(
} }
} }
if (hidden.length || hasServerHiddenReplies) { if (hidden.length || hasServerHiddenItems) {
if (hiddenRepliesVisible) { if (hiddenItemsVisible) {
return items.concat(hidden) return items.concat(hidden)
} else { } else {
return items.concat({ return items.concat({
type: 'showHiddenReplies', type: 'showHiddenReplies',
key: 'showHiddenReplies', key: 'showHiddenReplies',
onLoad: loadHiddenReplies, onLoad: loadServerHiddenItems,
}) })
} }
} }
@@ -456,14 +452,3 @@ export function getBranch(
length: end - branchStartIndex, length: end - branchStartIndex,
} }
} }
export function getModerationState(moderation: ModerationDecision) {
const modui = moderation.ui('contentList')
const blurred = modui.blur || modui.filter
const muted = (modui.blurs[0] || modui.filters[0])?.type === 'muted'
return {
blurred,
muted,
modui,
}
}
+9
View File
@@ -1,6 +1,7 @@
import { import {
type AppBskyFeedDefs, type AppBskyFeedDefs,
type AppBskyFeedPost, type AppBskyFeedPost,
type AppBskyFeedThreadgate,
type AppBskyUnspeccedDefs, type AppBskyUnspeccedDefs,
type AppBskyUnspeccedGetPostThreadHiddenV2, type AppBskyUnspeccedGetPostThreadHiddenV2,
type AppBskyUnspeccedGetPostThreadV2, type AppBskyUnspeccedGetPostThreadV2,
@@ -29,6 +30,14 @@ export type PostThreadParams = Pick<
view: 'tree' | 'linear' view: 'tree' | 'linear'
} }
export type UsePostThreadQueryResult = {
hasHiddenReplies: boolean
thread: AppBskyUnspeccedGetPostThreadV2.ThreadItem[]
threadgate?: Omit<AppBskyFeedDefs.ThreadgateView, 'record'> & {
record: AppBskyFeedThreadgate.Record
}
}
export type ThreadItem = export type ThreadItem =
| { | {
type: 'threadPost' type: 'threadPost'