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>
This commit is contained in:
		
							parent
							
								
									d2c42cf169
								
							
						
					
					
						commit
						f7ee532a85
					
				
					 9 changed files with 311 additions and 67 deletions
				
			
		|  | @ -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} | ||||
|  |  | |||
							
								
								
									
										61
									
								
								src/view/com/post-thread/PostThreadShowHiddenReplies.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								src/view/com/post-thread/PostThreadShowHiddenReplies.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -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…
	
	Add table
		Add a link
		
	
		Reference in a new issue