Improve moderation behaviors: show alert/inform sources and improve UX around threads (#3677)
* Dont show account or profile alerts and informs on posts * Sort threads to put blurred items at bottom * Group blurred replies under a single 'show hidden replies' control * Distinguish between muted and hidden replies in the thread view * Fix types * Modify the label alerts with some minor aesthetic updates and to show the source of a label * Tune when an account-level alert is shown on a post * Revert: show account-level alerts on posts again * Rm unused import * Fix to showing hidden replies when viewing a blurred item * Go ahead and uncover replies when 'show hidden posts' is clicked --------- Co-authored-by: dan <dan.abramov@gmail.com>zio/stable
parent
d2c42cf169
commit
f7ee532a85
|
@ -1,16 +1,16 @@
|
|||
import React from 'react'
|
||||
import {StyleProp, View, ViewStyle} from 'react-native'
|
||||
import {ModerationUI, ModerationCause} from '@atproto/api'
|
||||
import {ModerationCause, ModerationUI} from '@atproto/api'
|
||||
|
||||
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
|
||||
import {getModerationCauseKey} from '#/lib/moderation'
|
||||
|
||||
import {atoms as a} from '#/alf'
|
||||
import {Button, ButtonText, ButtonIcon} from '#/components/Button'
|
||||
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button} from '#/components/Button'
|
||||
import {
|
||||
ModerationDetailsDialog,
|
||||
useModerationDetailsDialogControl,
|
||||
} from '#/components/moderation/ModerationDetailsDialog'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function PostAlerts({
|
||||
modui,
|
||||
|
@ -41,23 +41,41 @@ export function PostAlerts({
|
|||
function PostLabel({cause}: {cause: ModerationCause}) {
|
||||
const control = useModerationDetailsDialogControl()
|
||||
const desc = useModerationCauseDescription(cause)
|
||||
const t = useTheme()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
label={desc.name}
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
size="small"
|
||||
shape="default"
|
||||
onPress={() => {
|
||||
control.open()
|
||||
}}
|
||||
style={[a.px_sm, a.py_xs, a.gap_xs]}>
|
||||
<ButtonIcon icon={desc.icon} position="left" />
|
||||
<ButtonText style={[a.text_left, a.leading_snug]}>
|
||||
{desc.name}
|
||||
</ButtonText>
|
||||
}}>
|
||||
{({hovered, pressed}) => (
|
||||
<View
|
||||
style={[
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
{paddingLeft: 4, paddingRight: 6, paddingVertical: 1},
|
||||
a.gap_xs,
|
||||
a.rounded_sm,
|
||||
hovered || pressed
|
||||
? t.atoms.bg_contrast_50
|
||||
: t.atoms.bg_contrast_25,
|
||||
]}>
|
||||
<desc.icon size="xs" fill={t.atoms.text_contrast_medium.color} />
|
||||
<Text
|
||||
style={[
|
||||
a.text_left,
|
||||
a.leading_snug,
|
||||
a.text_xs,
|
||||
t.atoms.text_contrast_medium,
|
||||
a.font_semibold,
|
||||
]}>
|
||||
{desc.name}
|
||||
{desc.source ? ` – ${desc.source}` : ''}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<ModerationDetailsDialog control={control} modcause={cause} />
|
||||
|
|
|
@ -18,6 +18,7 @@ import {
|
|||
import {Text} from '#/components/Typography'
|
||||
|
||||
interface Props extends ComponentProps<typeof Link> {
|
||||
disabled: boolean
|
||||
iconSize: number
|
||||
iconStyles: StyleProp<ViewStyle>
|
||||
modui: ModerationUI
|
||||
|
@ -27,6 +28,7 @@ interface Props extends ComponentProps<typeof Link> {
|
|||
export function PostHider({
|
||||
testID,
|
||||
href,
|
||||
disabled,
|
||||
modui,
|
||||
style,
|
||||
children,
|
||||
|
@ -47,7 +49,7 @@ export function PostHider({
|
|||
precacheProfile(queryClient, profile)
|
||||
}, [queryClient, profile])
|
||||
|
||||
if (!blur) {
|
||||
if (!blur || (disabled && !modui.noOverride)) {
|
||||
return (
|
||||
<Link
|
||||
testID={testID}
|
||||
|
|
|
@ -2,15 +2,15 @@ import React from 'react'
|
|||
import {StyleProp, View, ViewStyle} from 'react-native'
|
||||
import {ModerationCause, ModerationDecision} from '@atproto/api'
|
||||
|
||||
import {getModerationCauseKey} from 'lib/moderation'
|
||||
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
|
||||
|
||||
import {atoms as a} from '#/alf'
|
||||
import {Button, ButtonText, ButtonIcon} from '#/components/Button'
|
||||
import {getModerationCauseKey} from 'lib/moderation'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button} from '#/components/Button'
|
||||
import {
|
||||
ModerationDetailsDialog,
|
||||
useModerationDetailsDialogControl,
|
||||
} from '#/components/moderation/ModerationDetailsDialog'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function ProfileHeaderAlerts({
|
||||
moderation,
|
||||
|
@ -39,6 +39,7 @@ export function ProfileHeaderAlerts({
|
|||
}
|
||||
|
||||
function ProfileLabel({cause}: {cause: ModerationCause}) {
|
||||
const t = useTheme()
|
||||
const control = useModerationDetailsDialogControl()
|
||||
const desc = useModerationCauseDescription(cause)
|
||||
|
||||
|
@ -46,18 +47,35 @@ function ProfileLabel({cause}: {cause: ModerationCause}) {
|
|||
<>
|
||||
<Button
|
||||
label={desc.name}
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
size="small"
|
||||
shape="default"
|
||||
onPress={() => {
|
||||
control.open()
|
||||
}}
|
||||
style={[a.px_sm, a.py_xs, a.gap_xs]}>
|
||||
<ButtonIcon icon={desc.icon} position="left" />
|
||||
<ButtonText style={[a.text_left, a.leading_snug]}>
|
||||
{desc.name}
|
||||
</ButtonText>
|
||||
}}>
|
||||
{({hovered, pressed}) => (
|
||||
<View
|
||||
style={[
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
{paddingLeft: 6, paddingRight: 8, paddingVertical: 4},
|
||||
a.gap_xs,
|
||||
a.rounded_md,
|
||||
hovered || pressed
|
||||
? t.atoms.bg_contrast_50
|
||||
: t.atoms.bg_contrast_25,
|
||||
]}>
|
||||
<desc.icon size="sm" fill={t.atoms.text_contrast_medium.color} />
|
||||
<Text
|
||||
style={[
|
||||
a.text_left,
|
||||
a.leading_snug,
|
||||
a.text_sm,
|
||||
t.atoms.text_contrast_medium,
|
||||
a.font_semibold,
|
||||
]}>
|
||||
{desc.name}
|
||||
{desc.source ? ` – ${desc.source}` : ''}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<ModerationDetailsDialog control={control} modcause={cause} />
|
||||
|
|
|
@ -3,9 +3,12 @@ import {
|
|||
AppBskyFeedDefs,
|
||||
AppBskyFeedGetPostThread,
|
||||
AppBskyFeedPost,
|
||||
ModerationDecision,
|
||||
ModerationOpts,
|
||||
} from '@atproto/api'
|
||||
import {QueryClient, useQuery, useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
|
||||
import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {findAllPostsInQueryData as findAllPostsInSearchQueryData} from 'state/queries/search-posts'
|
||||
|
@ -21,8 +24,6 @@ export interface ThreadCtx {
|
|||
depth: number
|
||||
isHighlightedPost?: boolean
|
||||
hasMore?: boolean
|
||||
showChildReplyLine?: boolean
|
||||
showParentReplyLine?: boolean
|
||||
isParentLoading?: boolean
|
||||
isChildLoading?: boolean
|
||||
}
|
||||
|
@ -63,6 +64,8 @@ export type ThreadNode =
|
|||
| ThreadBlocked
|
||||
| ThreadUnknown
|
||||
|
||||
export type ThreadModerationCache = WeakMap<ThreadNode, ModerationDecision>
|
||||
|
||||
export function usePostThreadQuery(uri: string | undefined) {
|
||||
const queryClient = useQueryClient()
|
||||
const {getAgent} = useAgent()
|
||||
|
@ -92,9 +95,28 @@ export function usePostThreadQuery(uri: string | undefined) {
|
|||
})
|
||||
}
|
||||
|
||||
export function fillThreadModerationCache(
|
||||
cache: ThreadModerationCache,
|
||||
node: ThreadNode,
|
||||
moderationOpts: ModerationOpts,
|
||||
) {
|
||||
if (node.type === 'post') {
|
||||
cache.set(node, moderatePost(node.post, moderationOpts))
|
||||
if (node.parent) {
|
||||
fillThreadModerationCache(cache, node.parent, moderationOpts)
|
||||
}
|
||||
if (node.replies) {
|
||||
for (const reply of node.replies) {
|
||||
fillThreadModerationCache(cache, reply, moderationOpts)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function sortThread(
|
||||
node: ThreadNode,
|
||||
opts: UsePreferencesQueryResponse['threadViewPrefs'],
|
||||
modCache: ThreadModerationCache,
|
||||
): ThreadNode {
|
||||
if (node.type !== 'post') {
|
||||
return node
|
||||
|
@ -117,6 +139,18 @@ export function sortThread(
|
|||
} else if (bIsByOp) {
|
||||
return 1 // op's own reply
|
||||
}
|
||||
|
||||
const aBlur = Boolean(modCache.get(a)?.ui('contentList').blur)
|
||||
const bBlur = Boolean(modCache.get(b)?.ui('contentList').blur)
|
||||
if (aBlur !== bBlur) {
|
||||
if (aBlur) {
|
||||
return 1
|
||||
}
|
||||
if (bBlur) {
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.prioritizeFollowedUsers) {
|
||||
const af = a.post.author.viewer?.following
|
||||
const bf = b.post.author.viewer?.following
|
||||
|
@ -126,6 +160,7 @@ export function sortThread(
|
|||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.sort === 'oldest') {
|
||||
return a.post.indexedAt.localeCompare(b.post.indexedAt)
|
||||
} else if (opts.sort === 'newest') {
|
||||
|
@ -141,7 +176,7 @@ export function sortThread(
|
|||
}
|
||||
return b.post.indexedAt.localeCompare(a.post.indexedAt)
|
||||
})
|
||||
node.replies.forEach(reply => sortThread(reply, opts))
|
||||
node.replies.forEach(reply => sortThread(reply, opts, modCache))
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
@ -188,12 +223,6 @@ function responseToThreadNodes(
|
|||
isHighlightedPost: depth === 0,
|
||||
hasMore:
|
||||
direction === 'down' && !node.replies?.length && !!node.replyCount,
|
||||
showChildReplyLine:
|
||||
direction === 'up' ||
|
||||
(direction === 'down' && !!node.replies?.length),
|
||||
showParentReplyLine:
|
||||
(direction === 'up' && !!node.parent) ||
|
||||
(direction === 'down' && depth !== 1),
|
||||
},
|
||||
}
|
||||
} else if (AppBskyFeedDefs.isBlockedPost(node)) {
|
||||
|
@ -296,8 +325,6 @@ function threadNodeToPlaceholderThread(
|
|||
depth: 0,
|
||||
isHighlightedPost: true,
|
||||
hasMore: false,
|
||||
showChildReplyLine: false,
|
||||
showParentReplyLine: false,
|
||||
isParentLoading: !!node.record.reply,
|
||||
isChildLoading: !!node.post.replyCount,
|
||||
},
|
||||
|
@ -319,8 +346,6 @@ function postViewToPlaceholderThread(
|
|||
depth: 0,
|
||||
isHighlightedPost: true,
|
||||
hasMore: false,
|
||||
showChildReplyLine: false,
|
||||
showParentReplyLine: false,
|
||||
isParentLoading: !!(post.record as AppBskyFeedPost.Record).reply,
|
||||
isChildLoading: true, // assume yes (show the spinner) just in case
|
||||
},
|
||||
|
@ -342,8 +367,6 @@ function embedViewRecordToPlaceholderThread(
|
|||
depth: 0,
|
||||
isHighlightedPost: true,
|
||||
hasMore: false,
|
||||
showChildReplyLine: false,
|
||||
showParentReplyLine: false,
|
||||
isParentLoading: !!(record.value as AppBskyFeedPost.Record).reply,
|
||||
isChildLoading: true, // not available, so assume yes (to show the spinner)
|
||||
},
|
||||
|
|
|
@ -10,8 +10,10 @@ import {ScrollProvider} from '#/lib/ScrollContext'
|
|||
import {isAndroid, isNative, isWeb} from '#/platform/detection'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {
|
||||
fillThreadModerationCache,
|
||||
sortThread,
|
||||
ThreadBlocked,
|
||||
ThreadModerationCache,
|
||||
ThreadNode,
|
||||
ThreadNotFound,
|
||||
ThreadPost,
|
||||
|
@ -31,6 +33,7 @@ import {List, ListMethods} from '../util/List'
|
|||
import {Text} from '../util/text/Text'
|
||||
import {ViewHeader} from '../util/ViewHeader'
|
||||
import {PostThreadItem} from './PostThreadItem'
|
||||
import {PostThreadShowHiddenReplies} from './PostThreadShowHiddenReplies'
|
||||
|
||||
// FlatList maintainVisibleContentPosition breaks if too many items
|
||||
// are prepended. This seems to be an optimal number based on *shrug*.
|
||||
|
@ -45,8 +48,21 @@ const MAINTAIN_VISIBLE_CONTENT_POSITION = {
|
|||
const TOP_COMPONENT = {_reactKey: '__top_component__'}
|
||||
const REPLY_PROMPT = {_reactKey: '__reply__'}
|
||||
const LOAD_MORE = {_reactKey: '__load_more__'}
|
||||
const SHOW_HIDDEN_REPLIES = {_reactKey: '__show_hidden_replies__'}
|
||||
const SHOW_MUTED_REPLIES = {_reactKey: '__show_muted_replies__'}
|
||||
|
||||
type YieldedItem = ThreadPost | ThreadBlocked | ThreadNotFound
|
||||
enum HiddenRepliesState {
|
||||
Hide,
|
||||
Show,
|
||||
ShowAndOverridePostHider,
|
||||
}
|
||||
|
||||
type YieldedItem =
|
||||
| ThreadPost
|
||||
| ThreadBlocked
|
||||
| ThreadNotFound
|
||||
| typeof SHOW_HIDDEN_REPLIES
|
||||
| typeof SHOW_MUTED_REPLIES
|
||||
type RowItem =
|
||||
| YieldedItem
|
||||
// TODO: TS doesn't actually enforce it's one of these, it only enforces matching shape.
|
||||
|
@ -79,6 +95,9 @@ export function PostThread({
|
|||
const {isMobile, isTabletOrMobile} = useWebMediaQueries()
|
||||
const initialNumToRender = useInitialNumToRender()
|
||||
const {height: windowHeight} = useWindowDimensions()
|
||||
const [hiddenRepliesState, setHiddenRepliesState] = React.useState(
|
||||
HiddenRepliesState.Hide,
|
||||
)
|
||||
|
||||
const {data: preferences} = usePreferencesQuery()
|
||||
const {
|
||||
|
@ -135,16 +154,33 @@ export function PostThread({
|
|||
// On the web this is not necessary because we can synchronously adjust the scroll in onContentSizeChange instead.
|
||||
const [deferParents, setDeferParents] = React.useState(isNative)
|
||||
|
||||
const threadModerationCache = React.useMemo(() => {
|
||||
const cache: ThreadModerationCache = new WeakMap()
|
||||
if (thread && moderationOpts) {
|
||||
fillThreadModerationCache(cache, thread, moderationOpts)
|
||||
}
|
||||
return cache
|
||||
}, [thread, moderationOpts])
|
||||
|
||||
const skeleton = React.useMemo(() => {
|
||||
const threadViewPrefs = preferences?.threadViewPrefs
|
||||
if (!threadViewPrefs || !thread) return null
|
||||
|
||||
return createThreadSkeleton(
|
||||
sortThread(thread, threadViewPrefs),
|
||||
sortThread(thread, threadViewPrefs, threadModerationCache),
|
||||
hasSession,
|
||||
treeView,
|
||||
threadModerationCache,
|
||||
hiddenRepliesState !== HiddenRepliesState.Hide,
|
||||
)
|
||||
}, [thread, preferences?.threadViewPrefs, hasSession, treeView])
|
||||
}, [
|
||||
thread,
|
||||
preferences?.threadViewPrefs,
|
||||
hasSession,
|
||||
treeView,
|
||||
threadModerationCache,
|
||||
hiddenRepliesState,
|
||||
])
|
||||
|
||||
const error = React.useMemo(() => {
|
||||
if (AppBskyFeedDefs.isNotFoundPost(thread)) {
|
||||
|
@ -301,6 +337,24 @@ export function PostThread({
|
|||
{!isMobile && <ComposePrompt onPressCompose={onPressReply} />}
|
||||
</View>
|
||||
)
|
||||
} else if (item === SHOW_HIDDEN_REPLIES) {
|
||||
return (
|
||||
<PostThreadShowHiddenReplies
|
||||
type="hidden"
|
||||
onPress={() =>
|
||||
setHiddenRepliesState(HiddenRepliesState.ShowAndOverridePostHider)
|
||||
}
|
||||
/>
|
||||
)
|
||||
} else if (item === SHOW_MUTED_REPLIES) {
|
||||
return (
|
||||
<PostThreadShowHiddenReplies
|
||||
type="muted"
|
||||
onPress={() =>
|
||||
setHiddenRepliesState(HiddenRepliesState.ShowAndOverridePostHider)
|
||||
}
|
||||
/>
|
||||
)
|
||||
} else if (isThreadNotFound(item)) {
|
||||
return (
|
||||
<View style={[pal.border, pal.viewLight, styles.itemContainer]}>
|
||||
|
@ -321,9 +375,12 @@ export function PostThread({
|
|||
const prev = isThreadPost(posts[index - 1])
|
||||
? (posts[index - 1] as ThreadPost)
|
||||
: undefined
|
||||
const next = isThreadPost(posts[index - 1])
|
||||
? (posts[index - 1] as ThreadPost)
|
||||
const next = isThreadPost(posts[index + 1])
|
||||
? (posts[index + 1] as ThreadPost)
|
||||
: undefined
|
||||
const showChildReplyLine = (next?.ctx.depth || 0) > item.ctx.depth
|
||||
const showParentReplyLine =
|
||||
(item.ctx.depth < 0 && !!item.parent) || item.ctx.depth > 1
|
||||
const hasUnrevealedParents =
|
||||
index === 0 &&
|
||||
skeleton?.parents &&
|
||||
|
@ -335,16 +392,20 @@ export function PostThread({
|
|||
<PostThreadItem
|
||||
post={item.post}
|
||||
record={item.record}
|
||||
moderation={threadModerationCache.get(item)}
|
||||
treeView={treeView}
|
||||
depth={item.ctx.depth}
|
||||
prevPost={prev}
|
||||
nextPost={next}
|
||||
isHighlightedPost={item.ctx.isHighlightedPost}
|
||||
hasMore={item.ctx.hasMore}
|
||||
showChildReplyLine={item.ctx.showChildReplyLine}
|
||||
showParentReplyLine={item.ctx.showParentReplyLine}
|
||||
hasPrecedingItem={
|
||||
!!prev?.ctx.showChildReplyLine || !!hasUnrevealedParents
|
||||
showChildReplyLine={showChildReplyLine}
|
||||
showParentReplyLine={showParentReplyLine}
|
||||
hasPrecedingItem={showParentReplyLine || !!hasUnrevealedParents}
|
||||
overrideBlur={
|
||||
hiddenRepliesState ===
|
||||
HiddenRepliesState.ShowAndOverridePostHider &&
|
||||
item.ctx.depth > 0
|
||||
}
|
||||
onPostReply={refetch}
|
||||
/>
|
||||
|
@ -368,6 +429,9 @@ export function PostThread({
|
|||
deferParents,
|
||||
treeView,
|
||||
refetch,
|
||||
threadModerationCache,
|
||||
hiddenRepliesState,
|
||||
setHiddenRepliesState,
|
||||
],
|
||||
)
|
||||
|
||||
|
@ -437,13 +501,23 @@ function createThreadSkeleton(
|
|||
node: ThreadNode,
|
||||
hasSession: boolean,
|
||||
treeView: boolean,
|
||||
modCache: ThreadModerationCache,
|
||||
showHiddenReplies: boolean,
|
||||
): ThreadSkeletonParts | null {
|
||||
if (!node) return null
|
||||
|
||||
return {
|
||||
parents: Array.from(flattenThreadParents(node, hasSession)),
|
||||
highlightedPost: node,
|
||||
replies: Array.from(flattenThreadReplies(node, hasSession, treeView)),
|
||||
replies: Array.from(
|
||||
flattenThreadReplies(
|
||||
node,
|
||||
hasSession,
|
||||
treeView,
|
||||
modCache,
|
||||
showHiddenReplies,
|
||||
),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -465,31 +539,76 @@ function* flattenThreadParents(
|
|||
}
|
||||
}
|
||||
|
||||
// The enum is ordered to make them easy to merge
|
||||
enum HiddenReplyType {
|
||||
None = 0,
|
||||
Muted = 1,
|
||||
Hidden = 2,
|
||||
}
|
||||
|
||||
function* flattenThreadReplies(
|
||||
node: ThreadNode,
|
||||
hasSession: boolean,
|
||||
treeView: boolean,
|
||||
): Generator<YieldedItem, void> {
|
||||
modCache: ThreadModerationCache,
|
||||
showHiddenReplies: boolean,
|
||||
): Generator<YieldedItem, HiddenReplyType> {
|
||||
if (node.type === 'post') {
|
||||
// dont show pwi-opted-out posts to logged out users
|
||||
if (!hasSession && hasPwiOptOut(node)) {
|
||||
return
|
||||
return HiddenReplyType.None
|
||||
}
|
||||
|
||||
// handle blurred items
|
||||
if (node.ctx.depth > 0) {
|
||||
const modui = modCache.get(node)?.ui('contentList')
|
||||
if (modui?.blur) {
|
||||
if (!showHiddenReplies || node.ctx.depth > 1) {
|
||||
if (modui.blurs[0].type === 'muted') {
|
||||
return HiddenReplyType.Muted
|
||||
}
|
||||
return HiddenReplyType.Hidden
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!node.ctx.isHighlightedPost) {
|
||||
yield node
|
||||
}
|
||||
|
||||
if (node.replies?.length) {
|
||||
let hiddenReplies = HiddenReplyType.None
|
||||
for (const reply of node.replies) {
|
||||
yield* flattenThreadReplies(reply, hasSession, treeView)
|
||||
let hiddenReply = yield* flattenThreadReplies(
|
||||
reply,
|
||||
hasSession,
|
||||
treeView,
|
||||
modCache,
|
||||
showHiddenReplies,
|
||||
)
|
||||
if (hiddenReply > hiddenReplies) {
|
||||
hiddenReplies = hiddenReply
|
||||
}
|
||||
if (!treeView && !node.ctx.isHighlightedPost) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// show control to enable hidden replies
|
||||
if (node.ctx.depth === 0) {
|
||||
if (hiddenReplies === HiddenReplyType.Muted) {
|
||||
yield SHOW_MUTED_REPLIES
|
||||
} else if (hiddenReplies === HiddenReplyType.Hidden) {
|
||||
yield SHOW_HIDDEN_REPLIES
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (node.type === 'not-found') {
|
||||
yield node
|
||||
} else if (node.type === 'blocked') {
|
||||
yield node
|
||||
}
|
||||
return HiddenReplyType.None
|
||||
}
|
||||
|
||||
function hasPwiOptOut(node: ThreadPost) {
|
||||
|
|
|
@ -11,11 +11,9 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
|||
import {msg, Plural, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
|
||||
import {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow'
|
||||
import {useLanguagePrefs} from '#/state/preferences'
|
||||
import {useOpenLink} from '#/state/preferences/in-app-browser'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {ThreadPost} from '#/state/queries/post-thread'
|
||||
import {useComposerControls} from '#/state/shell/composer'
|
||||
import {MAX_POST_LINES} from 'lib/constants'
|
||||
|
@ -50,6 +48,7 @@ import {PreviewableUserAvatar} from '../util/UserAvatar'
|
|||
export function PostThreadItem({
|
||||
post,
|
||||
record,
|
||||
moderation,
|
||||
treeView,
|
||||
depth,
|
||||
prevPost,
|
||||
|
@ -59,10 +58,12 @@ export function PostThreadItem({
|
|||
showChildReplyLine,
|
||||
showParentReplyLine,
|
||||
hasPrecedingItem,
|
||||
overrideBlur,
|
||||
onPostReply,
|
||||
}: {
|
||||
post: AppBskyFeedDefs.PostView
|
||||
record: AppBskyFeedPost.Record
|
||||
moderation: ModerationDecision | undefined
|
||||
treeView: boolean
|
||||
depth: number
|
||||
prevPost: ThreadPost | undefined
|
||||
|
@ -72,9 +73,9 @@ export function PostThreadItem({
|
|||
showChildReplyLine?: boolean
|
||||
showParentReplyLine?: boolean
|
||||
hasPrecedingItem: boolean
|
||||
overrideBlur: boolean
|
||||
onPostReply: () => void
|
||||
}) {
|
||||
const moderationOpts = useModerationOpts()
|
||||
const postShadowed = usePostShadow(post)
|
||||
const richText = useMemo(
|
||||
() =>
|
||||
|
@ -84,11 +85,6 @@ export function PostThreadItem({
|
|||
}),
|
||||
[record],
|
||||
)
|
||||
const moderation = useMemo(
|
||||
() =>
|
||||
post && moderationOpts ? moderatePost(post, moderationOpts) : undefined,
|
||||
[post, moderationOpts],
|
||||
)
|
||||
if (postShadowed === POST_TOMBSTONE) {
|
||||
return <PostThreadItemDeleted />
|
||||
}
|
||||
|
@ -110,6 +106,7 @@ export function PostThreadItem({
|
|||
showChildReplyLine={showChildReplyLine}
|
||||
showParentReplyLine={showParentReplyLine}
|
||||
hasPrecedingItem={hasPrecedingItem}
|
||||
overrideBlur={overrideBlur}
|
||||
onPostReply={onPostReply}
|
||||
/>
|
||||
)
|
||||
|
@ -143,6 +140,7 @@ let PostThreadItemLoaded = ({
|
|||
showChildReplyLine,
|
||||
showParentReplyLine,
|
||||
hasPrecedingItem,
|
||||
overrideBlur,
|
||||
onPostReply,
|
||||
}: {
|
||||
post: Shadow<AppBskyFeedDefs.PostView>
|
||||
|
@ -158,6 +156,7 @@ let PostThreadItemLoaded = ({
|
|||
showChildReplyLine?: boolean
|
||||
showParentReplyLine?: boolean
|
||||
hasPrecedingItem: boolean
|
||||
overrideBlur: boolean
|
||||
onPostReply: () => void
|
||||
}): React.ReactNode => {
|
||||
const pal = usePalette('default')
|
||||
|
@ -394,6 +393,7 @@ let PostThreadItemLoaded = ({
|
|||
<PostHider
|
||||
testID={`postThreadItem-by-${post.author.handle}`}
|
||||
href={postHref}
|
||||
disabled={overrideBlur}
|
||||
style={[pal.view]}
|
||||
modui={moderation.ui('contentList')}
|
||||
iconSize={isThreadedChild ? 26 : 38}
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
import * as React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button} from '#/components/Button'
|
||||
import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function PostThreadShowHiddenReplies({
|
||||
type,
|
||||
onPress,
|
||||
}: {
|
||||
type: 'hidden' | 'muted'
|
||||
onPress: () => void
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
const label =
|
||||
type === 'muted' ? _(msg`Show muted replies`) : _(msg`Show hidden replies`)
|
||||
|
||||
return (
|
||||
<Button onPress={onPress} label={label}>
|
||||
{({hovered, pressed}) => (
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.gap_sm,
|
||||
a.py_lg,
|
||||
a.px_xl,
|
||||
a.border_t,
|
||||
t.atoms.border_contrast_low,
|
||||
hovered || pressed ? t.atoms.bg_contrast_25 : t.atoms.bg,
|
||||
]}>
|
||||
<View
|
||||
style={[
|
||||
t.atoms.bg_contrast_25,
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
{
|
||||
width: 26,
|
||||
height: 26,
|
||||
borderRadius: 13,
|
||||
marginRight: 4,
|
||||
},
|
||||
]}>
|
||||
<EyeSlash size="sm" fill={t.atoms.text_contrast_medium.color} />
|
||||
</View>
|
||||
<Text
|
||||
style={[t.atoms.text_contrast_medium, a.flex_1]}
|
||||
numberOfLines={1}>
|
||||
{label}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</Button>
|
||||
)
|
||||
}
|
|
@ -367,7 +367,7 @@ let PostContent = ({
|
|||
modui={moderation.ui('contentList')}
|
||||
ignoreMute
|
||||
childContainerStyle={styles.contentHiderChild}>
|
||||
<PostAlerts modui={moderation.ui('contentList')} style={[a.py_xs]} />
|
||||
<PostAlerts modui={moderation.ui('contentList')} style={[a.pb_xs]} />
|
||||
{richText.text ? (
|
||||
<View style={styles.postTextContainer}>
|
||||
<RichText
|
||||
|
|
|
@ -813,6 +813,7 @@ function MockPostFeedItem({
|
|||
|
||||
function MockPostThreadItem({
|
||||
post,
|
||||
moderation,
|
||||
reply,
|
||||
}: {
|
||||
post: AppBskyFeedDefs.PostView
|
||||
|
@ -824,12 +825,14 @@ function MockPostThreadItem({
|
|||
// @ts-ignore
|
||||
post={post}
|
||||
record={post.record as AppBskyFeedPost.Record}
|
||||
moderation={moderation}
|
||||
depth={reply ? 1 : 0}
|
||||
isHighlightedPost={!reply}
|
||||
treeView={false}
|
||||
prevPost={undefined}
|
||||
nextPost={undefined}
|
||||
hasPrecedingItem={false}
|
||||
overrideBlur={false}
|
||||
onPostReply={() => {}}
|
||||
/>
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue