Rough handling of hidden/muted

This commit is contained in:
Eric Bailey
2025-05-06 18:52:46 -05:00
parent e42cd77fc0
commit df607870f0
2 changed files with 115 additions and 66 deletions
+96 -59
View File
@@ -43,7 +43,7 @@ import {
findAllProfilesInQueryData as findAllProfilesInFeedQueryData, findAllProfilesInQueryData as findAllProfilesInFeedQueryData,
} from './post-feed' } from './post-feed'
export type PostThreadV2Options = { export type PostThreadV2Params = {
view: 'tree' | 'linear' view: 'tree' | 'linear'
sort: 'hotness' | 'oldest' | 'newest' | 'most-likes' | 'random' | string sort: 'hotness' | 'oldest' | 'newest' | 'most-likes' | 'random' | string
prioritizeFollows: BskyThreadViewPreference['prioritizeFollowedUsers'] prioritizeFollows: BskyThreadViewPreference['prioritizeFollowedUsers']
@@ -51,13 +51,16 @@ export type PostThreadV2Options = {
export const getPostThreadV2QueryKeyRoot = 'getPostThreadV2' as const export const getPostThreadV2QueryKeyRoot = 'getPostThreadV2' as const
export const createGetPostThreadV2QueryKey = ( export const createGetPostThreadV2QueryKey = (
props: Pick<GetPostThreadV2Params, 'uri' | 'options'>, props: Pick<GetPostThreadV2QueryProps, 'uri' | 'params'>,
) => [getPostThreadV2QueryKeyRoot, props] as const ) => [getPostThreadV2QueryKeyRoot, props] as const
export type GetPostThreadV2Params = { export type GetPostThreadV2QueryProps = {
uri?: string uri?: string
enabled?: boolean enabled?: boolean
options: PostThreadV2Options params: PostThreadV2Params
state: {
shownHiddenReplyKinds: Set<HiddenReplyKind>
}
} }
export type GetPostThreadV2QueryData = { export type GetPostThreadV2QueryData = {
@@ -65,7 +68,7 @@ export type GetPostThreadV2QueryData = {
threadgate?: AppBskyFeedDefs.ThreadgateView threadgate?: AppBskyFeedDefs.ThreadgateView
} }
export function mapSortOptionsToSortID(sort: PostThreadV2Options['sort']) { export function mapSortOptionsToSortID(sort: PostThreadV2Params['sort']) {
switch (sort) { switch (sort) {
case 'hotness': case 'hotness':
return APP_BSKY_FEED.GetPostThreadV2Hotness return APP_BSKY_FEED.GetPostThreadV2Hotness
@@ -83,8 +86,9 @@ export function mapSortOptionsToSortID(sort: PostThreadV2Options['sort']) {
export function useGetPostThreadV2({ export function useGetPostThreadV2({
uri, uri,
enabled: isEnabled, enabled: isEnabled,
options, params,
}: GetPostThreadV2Params) { state,
}: GetPostThreadV2QueryProps) {
const qc = useQueryClient() const qc = useQueryClient()
const agent = useAgent() const agent = useAgent()
const {hasSession} = useSession() const {hasSession} = useSession()
@@ -97,13 +101,13 @@ export function useGetPostThreadV2({
enabled, enabled,
queryKey: createGetPostThreadV2QueryKey({ queryKey: createGetPostThreadV2QueryKey({
uri, uri,
options, params,
}), }),
async queryFn() { async queryFn() {
const {data} = await agent.app.bsky.feed.getPostThreadV2({ const {data} = await agent.app.bsky.feed.getPostThreadV2({
uri: uri!, uri: uri!,
below: 10, below: 10,
sorting: mapSortOptionsToSortID(options.sort), sorting: mapSortOptionsToSortID(params.sort),
}) })
return data return data
}, },
@@ -136,12 +140,12 @@ export function useGetPostThreadV2({
const filtered = filterAndSort(query.data?.thread || [], { const filtered = filterAndSort(query.data?.thread || [], {
hasSession, hasSession,
options, params,
threadgateHiddenReplies: mergeThreadgateHiddenReplies( threadgateHiddenReplies: mergeThreadgateHiddenReplies(
query.data?.threadgate?.record, query.data?.threadgate?.record,
), ),
moderationOpts: moderationOpts!, moderationOpts: moderationOpts!,
showHiddenReplies: false, shownHiddenReplyKinds: state.shownHiddenReplyKinds,
}) })
return { return {
@@ -251,20 +255,24 @@ export function filterAndSort(
thread: AppBskyFeedGetPostThreadV2.OutputSchema['thread'], thread: AppBskyFeedGetPostThreadV2.OutputSchema['thread'],
{ {
hasSession, hasSession,
options, params,
threadgateHiddenReplies, threadgateHiddenReplies,
moderationOpts, moderationOpts,
showHiddenReplies = false, shownHiddenReplyKinds,
}: { }: {
hasSession: boolean hasSession: boolean
options: PostThreadV2Options params: PostThreadV2Params
threadgateHiddenReplies: Set<string> threadgateHiddenReplies: Set<string>
moderationOpts: ModerationOpts moderationOpts: ModerationOpts
showHiddenReplies: boolean shownHiddenReplyKinds: Set<HiddenReplyKind>
}, },
) { ) {
const sorted: Slice[] = [] const slices: Slice[] = []
const hidden: Slice[] = [] const hidden: Slice[] = []
const muted: Slice[] = []
const showMuted = shownHiddenReplyKinds.has(HiddenReplyKind.Muted)
const showHidden = shownHiddenReplyKinds.has(HiddenReplyKind.Hidden)
traversal: for (let i = 0; i < thread.length; i++) { traversal: for (let i = 0; i < thread.length; i++) {
const item = thread[i] const item = thread[i]
@@ -282,13 +290,13 @@ export function filterAndSort(
*/ */
} else if (item.depth === 0) { } else if (item.depth === 0) {
if (AppBskyFeedDefs.isThreadItemNoUnauthenticated(item)) { if (AppBskyFeedDefs.isThreadItemNoUnauthenticated(item)) {
sorted.push(views.noUnauthenticated({item})) slices.push(views.noUnauthenticated({item}))
} else if (AppBskyFeedDefs.isThreadItemNotFound(item)) { } else if (AppBskyFeedDefs.isThreadItemNotFound(item)) {
sorted.push(views.notFound({item})) slices.push(views.notFound({item}))
} else if (AppBskyFeedDefs.isThreadItemBlocked(item)) { } else if (AppBskyFeedDefs.isThreadItemBlocked(item)) {
sorted.push(views.blocked({item})) slices.push(views.blocked({item}))
} else if (AppBskyFeedDefs.isThreadItemPost(item)) { } else if (AppBskyFeedDefs.isThreadItemPost(item)) {
sorted.push( slices.push(
views.post({ views.post({
item, item,
oneUp: oneUp, oneUp: oneUp,
@@ -298,7 +306,7 @@ export function filterAndSort(
) )
if (hasSession) { if (hasSession) {
sorted.push({ slices.push({
type: 'replyComposer', type: 'replyComposer',
key: 'replyComposer', key: 'replyComposer',
}) })
@@ -310,16 +318,16 @@ export function filterAndSort(
const parentOneUp = thread[pi - 1] const parentOneUp = thread[pi - 1]
if (AppBskyFeedDefs.isThreadItemNoUnauthenticated(parent)) { if (AppBskyFeedDefs.isThreadItemNoUnauthenticated(parent)) {
sorted.unshift(views.noUnauthenticated({item: parent})) slices.unshift(views.noUnauthenticated({item: parent}))
break parentTraversal break parentTraversal
} else if (AppBskyFeedDefs.isThreadItemNotFound(parent)) { } else if (AppBskyFeedDefs.isThreadItemNotFound(parent)) {
sorted.unshift(views.notFound({item: parent})) slices.unshift(views.notFound({item: parent}))
break parentTraversal break parentTraversal
} else if (AppBskyFeedDefs.isThreadItemBlocked(parent)) { } else if (AppBskyFeedDefs.isThreadItemBlocked(parent)) {
sorted.unshift(views.blocked({item: parent})) slices.unshift(views.blocked({item: parent}))
break parentTraversal break parentTraversal
} else if (AppBskyFeedDefs.isThreadItemPost(parent)) { } else if (AppBskyFeedDefs.isThreadItemPost(parent)) {
sorted.unshift( slices.unshift(
views.post({ views.post({
item, item,
oneUp: parentOneUp, oneUp: parentOneUp,
@@ -347,9 +355,18 @@ export function filterAndSort(
i = branch.end i = branch.end
continue traversal continue traversal
} else if (AppBskyFeedDefs.isThreadItemPost(item)) { } else if (AppBskyFeedDefs.isThreadItemPost(item)) {
const post = views.post({
item,
oneUp,
oneDown,
moderationOpts,
})
const modui = post.moderation.ui('contentList')
const isBlurred = modui.blur || modui.filter
const isMuted = (modui.blurs[0] || modui.filters[0])?.type === 'muted'
const isHidden = threadgateHiddenReplies.has(item.uri) const isHidden = threadgateHiddenReplies.has(item.uri)
if (isHidden) { if (isHidden || isBlurred || isMuted) {
const branch = getSubBranch(thread, i, item.depth) const branch = getSubBranch(thread, i, item.depth)
/* /*
@@ -360,30 +377,29 @@ export function filterAndSort(
const next = thread[ci] const next = thread[ci]
if (AppBskyFeedDefs.isThreadItemPost(next)) { if (AppBskyFeedDefs.isThreadItemPost(next)) {
hidden.push( const post = views.post({
views.post({ item: next,
item: next, oneUp: oneUp,
oneUp: oneUp, oneDown: oneDown,
oneDown: oneDown, moderationOpts,
moderationOpts, })
}),
) if (isMuted) {
muted.push(post)
} else {
hidden.push(post)
}
} else { } else {
break break
} }
} }
} else if (showHiddenReplies) { } else {
/* /*
* Nested hidden replies either filter entirely or show in situ * Nested hidden replies either filter entirely or show in situ
*/ */
sorted.push( if ((isMuted && showMuted) || showHidden) {
views.post({ slices.push(post)
item, }
oneUp,
oneDown,
moderationOpts,
}),
)
} }
/* /*
@@ -395,24 +411,49 @@ export function filterAndSort(
/* /*
* Not hidden, so show it * Not hidden, so show it
*/ */
sorted.push( slices.push(post)
views.post({
item,
oneUp,
oneDown,
moderationOpts,
}),
)
} }
} }
} }
} }
if (showHiddenReplies) { const [hiddenKind1, hiddenKind2] = Array.from(shownHiddenReplyKinds)
return sorted.concat(hidden)
if (hiddenKind1 === HiddenReplyKind.Hidden) {
slices.push(...hidden)
}
if (hiddenKind1 === HiddenReplyKind.Muted) {
slices.push(...muted)
}
if (hiddenKind2 === HiddenReplyKind.Hidden) {
slices.push(...hidden)
}
if (hiddenKind2 === HiddenReplyKind.Muted) {
slices.push(...muted)
} }
return sorted if (muted.length && !showMuted) {
slices.push({
type: 'showHiddenReplies',
key: 'showMutedReplies',
kind: HiddenReplyKind.Muted,
})
}
if (hidden.length && !showHidden) {
slices.push({
type: 'showHiddenReplies',
key: 'showHiddenReplies',
kind: HiddenReplyKind.Hidden,
})
}
return slices
}
export enum HiddenReplyKind {
Hidden = 'hidden',
Muted = 'muted',
} }
export type Slice = export type Slice =
@@ -453,11 +494,7 @@ export type Slice =
| { | {
type: 'showHiddenReplies' type: 'showHiddenReplies'
key: string key: string
} kind: HiddenReplyKind
| {
// TODO needed?
type: 'showMutedReplies'
key: string
} }
function getThreadgate( function getThreadgate(
+19 -7
View File
@@ -16,7 +16,7 @@ import * as Layout from '#/components/Layout'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
import {useGetPostThreadV2, Slice} from '#/state/queries/useGetPostThreadV2' import {useGetPostThreadV2, Slice, HiddenReplyKind} from '#/state/queries/useGetPostThreadV2'
import * as Menu from '#/components/Menu' import * as Menu from '#/components/Menu'
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'
@@ -28,6 +28,7 @@ import {PostThreadItem} from '#/view/com/post-thread/PostThreadItem'
import {PostThreadComposePrompt} from '#/view/com/post-thread/PostThreadComposePrompt' import {PostThreadComposePrompt} from '#/view/com/post-thread/PostThreadComposePrompt'
import {usePreferencesQuery} from '#/state/queries/preferences' import {usePreferencesQuery} from '#/state/queries/preferences'
import {useComposerControls} from '#/state/shell' import {useComposerControls} from '#/state/shell'
import {PostThreadShowHiddenReplies} from '#/view/com/post-thread/PostThreadShowHiddenReplies'
const MAINTAIN_VISIBLE_CONTENT_POSITION = { const MAINTAIN_VISIBLE_CONTENT_POSITION = {
// We don't insert any elements before the root row while loading. // We don't insert any elements before the root row while loading.
@@ -126,14 +127,20 @@ export function Inner({uri}: {uri: string | undefined}) {
setTreeViewEnabled, setTreeViewEnabled,
} = useThreadPreferences() } = useThreadPreferences()
const [showHiddenReplies, setShowHiddenReplies] = useState<HiddenReplyKind | null>(null)
const [shownHiddenReplyKinds, setShownHiddenReplyKinds] = useState<Set<HiddenReplyKind>>(new Set())
const {isFetching, isPlaceholderData, error, data, refetch} = useGetPostThreadV2({ const {isFetching, isPlaceholderData, error, data, refetch} = useGetPostThreadV2({
uri, uri,
enabled: isThreadPreferencesLoaded, enabled: isThreadPreferencesLoaded,
options: { params: {
sort: sortReplies, sort: sortReplies,
view: treeViewEnabled ? 'tree' : 'linear', view: treeViewEnabled ? 'tree' : 'linear',
prioritizeFollows: prioritizeFollowedUsers, prioritizeFollows: prioritizeFollowedUsers,
}, },
state: {
shownHiddenReplyKinds,
}
}) })
const ref = useRef<ListMethods>(null) const ref = useRef<ListMethods>(null)
@@ -199,11 +206,9 @@ export function Inner({uri}: {uri: string | undefined}) {
hasPrecedingItem={ hasPrecedingItem={
item.ui.showParentReplyLine || !!item.slice.hasUnhydratedParents item.ui.showParentReplyLine || !!item.slice.hasUnhydratedParents
} // !!hasUnrevealedParents // TODO } // !!hasUnrevealedParents // TODO
// overrideBlur={ overrideBlur={
// hiddenRepliesState === shownHiddenReplyKinds.has(HiddenReplyKind.Muted) && item.slice.depth > 0
// HiddenRepliesState.ShowAndOverridePostHider && }
// item.ctx.depth > 0
// }
onPostReply={onPostReply} onPostReply={onPostReply}
hideTopBorder={index === 0} // && !item.slice.isParentLoading} // TODO hideTopBorder={index === 0} // && !item.slice.isParentLoading} // TODO
/> />
@@ -215,6 +220,13 @@ export function Inner({uri}: {uri: string | undefined}) {
{gtPhone && <PostThreadComposePrompt onPressCompose={onReplyToAnchor} />} {gtPhone && <PostThreadComposePrompt onPressCompose={onReplyToAnchor} />}
</View> </View>
) )
} else if (item.type === 'showHiddenReplies') {
return (
<PostThreadShowHiddenReplies
type={item.kind === 'muted' ? 'muted' : 'hidden'}
onPress={() => setShownHiddenReplyKinds(kinds => new Set([...kinds, item.kind]))}
/>
)
} }
return null return null
} }