diff options
Diffstat (limited to 'src/view')
-rw-r--r-- | src/view/com/post-thread/PostThread.tsx | 145 | ||||
-rw-r--r-- | src/view/com/post-thread/PostThreadItem.tsx | 16 | ||||
-rw-r--r-- | src/view/com/post-thread/PostThreadShowHiddenReplies.tsx | 61 | ||||
-rw-r--r-- | src/view/com/posts/FeedItem.tsx | 2 | ||||
-rw-r--r-- | src/view/screens/DebugMod.tsx | 3 |
5 files changed, 205 insertions, 22 deletions
diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx index a52818fd1..4f7d0d3c6 100644 --- a/src/view/com/post-thread/PostThread.tsx +++ b/src/view/com/post-thread/PostThread.tsx @@ -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) { diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index f644a5366..c44875b37 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -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} diff --git a/src/view/com/post-thread/PostThreadShowHiddenReplies.tsx b/src/view/com/post-thread/PostThreadShowHiddenReplies.tsx new file mode 100644 index 000000000..998906524 --- /dev/null +++ b/src/view/com/post-thread/PostThreadShowHiddenReplies.tsx @@ -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> + ) +} diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx index 0decb81df..6e7c1c7eb 100644 --- a/src/view/com/posts/FeedItem.tsx +++ b/src/view/com/posts/FeedItem.tsx @@ -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 diff --git a/src/view/screens/DebugMod.tsx b/src/view/screens/DebugMod.tsx index 77b07b8c9..7d0d2fb03 100644 --- a/src/view/screens/DebugMod.tsx +++ b/src/view/screens/DebugMod.tsx @@ -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={() => {}} /> ) |