Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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>

authored by

Paul Frazee
dan
and committed by
GitHub
f7ee532a d2c42cf1

+311 -67
+33 -15
src/components/moderation/PostAlerts.tsx
··· 1 1 import React from 'react' 2 2 import {StyleProp, View, ViewStyle} from 'react-native' 3 - import {ModerationUI, ModerationCause} from '@atproto/api' 3 + import {ModerationCause, ModerationUI} from '@atproto/api' 4 4 5 - import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription' 6 5 import {getModerationCauseKey} from '#/lib/moderation' 7 - 8 - import {atoms as a} from '#/alf' 9 - import {Button, ButtonText, ButtonIcon} from '#/components/Button' 6 + import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription' 7 + import {atoms as a, useTheme} from '#/alf' 8 + import {Button} from '#/components/Button' 10 9 import { 11 10 ModerationDetailsDialog, 12 11 useModerationDetailsDialogControl, 13 12 } from '#/components/moderation/ModerationDetailsDialog' 13 + import {Text} from '#/components/Typography' 14 14 15 15 export function PostAlerts({ 16 16 modui, ··· 41 41 function PostLabel({cause}: {cause: ModerationCause}) { 42 42 const control = useModerationDetailsDialogControl() 43 43 const desc = useModerationCauseDescription(cause) 44 + const t = useTheme() 44 45 45 46 return ( 46 47 <> 47 48 <Button 48 49 label={desc.name} 49 - variant="solid" 50 - color="secondary" 51 - size="small" 52 - shape="default" 53 50 onPress={() => { 54 51 control.open() 55 - }} 56 - style={[a.px_sm, a.py_xs, a.gap_xs]}> 57 - <ButtonIcon icon={desc.icon} position="left" /> 58 - <ButtonText style={[a.text_left, a.leading_snug]}> 59 - {desc.name} 60 - </ButtonText> 52 + }}> 53 + {({hovered, pressed}) => ( 54 + <View 55 + style={[ 56 + a.flex_row, 57 + a.align_center, 58 + {paddingLeft: 4, paddingRight: 6, paddingVertical: 1}, 59 + a.gap_xs, 60 + a.rounded_sm, 61 + hovered || pressed 62 + ? t.atoms.bg_contrast_50 63 + : t.atoms.bg_contrast_25, 64 + ]}> 65 + <desc.icon size="xs" fill={t.atoms.text_contrast_medium.color} /> 66 + <Text 67 + style={[ 68 + a.text_left, 69 + a.leading_snug, 70 + a.text_xs, 71 + t.atoms.text_contrast_medium, 72 + a.font_semibold, 73 + ]}> 74 + {desc.name} 75 + {desc.source ? ` – ${desc.source}` : ''} 76 + </Text> 77 + </View> 78 + )} 61 79 </Button> 62 80 63 81 <ModerationDetailsDialog control={control} modcause={cause} />
+3 -1
src/components/moderation/PostHider.tsx
··· 18 18 import {Text} from '#/components/Typography' 19 19 20 20 interface Props extends ComponentProps<typeof Link> { 21 + disabled: boolean 21 22 iconSize: number 22 23 iconStyles: StyleProp<ViewStyle> 23 24 modui: ModerationUI ··· 27 28 export function PostHider({ 28 29 testID, 29 30 href, 31 + disabled, 30 32 modui, 31 33 style, 32 34 children, ··· 47 49 precacheProfile(queryClient, profile) 48 50 }, [queryClient, profile]) 49 51 50 - if (!blur) { 52 + if (!blur || (disabled && !modui.noOverride)) { 51 53 return ( 52 54 <Link 53 55 testID={testID}
+32 -14
src/components/moderation/ProfileHeaderAlerts.tsx
··· 2 2 import {StyleProp, View, ViewStyle} from 'react-native' 3 3 import {ModerationCause, ModerationDecision} from '@atproto/api' 4 4 5 - import {getModerationCauseKey} from 'lib/moderation' 6 5 import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription' 7 - 8 - import {atoms as a} from '#/alf' 9 - import {Button, ButtonText, ButtonIcon} from '#/components/Button' 6 + import {getModerationCauseKey} from 'lib/moderation' 7 + import {atoms as a, useTheme} from '#/alf' 8 + import {Button} from '#/components/Button' 10 9 import { 11 10 ModerationDetailsDialog, 12 11 useModerationDetailsDialogControl, 13 12 } from '#/components/moderation/ModerationDetailsDialog' 13 + import {Text} from '#/components/Typography' 14 14 15 15 export function ProfileHeaderAlerts({ 16 16 moderation, ··· 39 39 } 40 40 41 41 function ProfileLabel({cause}: {cause: ModerationCause}) { 42 + const t = useTheme() 42 43 const control = useModerationDetailsDialogControl() 43 44 const desc = useModerationCauseDescription(cause) 44 45 ··· 46 47 <> 47 48 <Button 48 49 label={desc.name} 49 - variant="solid" 50 - color="secondary" 51 - size="small" 52 - shape="default" 53 50 onPress={() => { 54 51 control.open() 55 - }} 56 - style={[a.px_sm, a.py_xs, a.gap_xs]}> 57 - <ButtonIcon icon={desc.icon} position="left" /> 58 - <ButtonText style={[a.text_left, a.leading_snug]}> 59 - {desc.name} 60 - </ButtonText> 52 + }}> 53 + {({hovered, pressed}) => ( 54 + <View 55 + style={[ 56 + a.flex_row, 57 + a.align_center, 58 + {paddingLeft: 6, paddingRight: 8, paddingVertical: 4}, 59 + a.gap_xs, 60 + a.rounded_md, 61 + hovered || pressed 62 + ? t.atoms.bg_contrast_50 63 + : t.atoms.bg_contrast_25, 64 + ]}> 65 + <desc.icon size="sm" fill={t.atoms.text_contrast_medium.color} /> 66 + <Text 67 + style={[ 68 + a.text_left, 69 + a.leading_snug, 70 + a.text_sm, 71 + t.atoms.text_contrast_medium, 72 + a.font_semibold, 73 + ]}> 74 + {desc.name} 75 + {desc.source ? ` – ${desc.source}` : ''} 76 + </Text> 77 + </View> 78 + )} 61 79 </Button> 62 80 63 81 <ModerationDetailsDialog control={control} modcause={cause} />
+38 -15
src/state/queries/post-thread.ts
··· 3 3 AppBskyFeedDefs, 4 4 AppBskyFeedGetPostThread, 5 5 AppBskyFeedPost, 6 + ModerationDecision, 7 + ModerationOpts, 6 8 } from '@atproto/api' 7 9 import {QueryClient, useQuery, useQueryClient} from '@tanstack/react-query' 8 10 11 + import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped' 9 12 import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types' 10 13 import {useAgent} from '#/state/session' 11 14 import {findAllPostsInQueryData as findAllPostsInSearchQueryData} from 'state/queries/search-posts' ··· 21 24 depth: number 22 25 isHighlightedPost?: boolean 23 26 hasMore?: boolean 24 - showChildReplyLine?: boolean 25 - showParentReplyLine?: boolean 26 27 isParentLoading?: boolean 27 28 isChildLoading?: boolean 28 29 } ··· 62 63 | ThreadNotFound 63 64 | ThreadBlocked 64 65 | ThreadUnknown 66 + 67 + export type ThreadModerationCache = WeakMap<ThreadNode, ModerationDecision> 65 68 66 69 export function usePostThreadQuery(uri: string | undefined) { 67 70 const queryClient = useQueryClient() ··· 92 95 }) 93 96 } 94 97 98 + export function fillThreadModerationCache( 99 + cache: ThreadModerationCache, 100 + node: ThreadNode, 101 + moderationOpts: ModerationOpts, 102 + ) { 103 + if (node.type === 'post') { 104 + cache.set(node, moderatePost(node.post, moderationOpts)) 105 + if (node.parent) { 106 + fillThreadModerationCache(cache, node.parent, moderationOpts) 107 + } 108 + if (node.replies) { 109 + for (const reply of node.replies) { 110 + fillThreadModerationCache(cache, reply, moderationOpts) 111 + } 112 + } 113 + } 114 + } 115 + 95 116 export function sortThread( 96 117 node: ThreadNode, 97 118 opts: UsePreferencesQueryResponse['threadViewPrefs'], 119 + modCache: ThreadModerationCache, 98 120 ): ThreadNode { 99 121 if (node.type !== 'post') { 100 122 return node ··· 117 139 } else if (bIsByOp) { 118 140 return 1 // op's own reply 119 141 } 142 + 143 + const aBlur = Boolean(modCache.get(a)?.ui('contentList').blur) 144 + const bBlur = Boolean(modCache.get(b)?.ui('contentList').blur) 145 + if (aBlur !== bBlur) { 146 + if (aBlur) { 147 + return 1 148 + } 149 + if (bBlur) { 150 + return -1 151 + } 152 + } 153 + 120 154 if (opts.prioritizeFollowedUsers) { 121 155 const af = a.post.author.viewer?.following 122 156 const bf = b.post.author.viewer?.following ··· 126 160 return 1 127 161 } 128 162 } 163 + 129 164 if (opts.sort === 'oldest') { 130 165 return a.post.indexedAt.localeCompare(b.post.indexedAt) 131 166 } else if (opts.sort === 'newest') { ··· 141 176 } 142 177 return b.post.indexedAt.localeCompare(a.post.indexedAt) 143 178 }) 144 - node.replies.forEach(reply => sortThread(reply, opts)) 179 + node.replies.forEach(reply => sortThread(reply, opts, modCache)) 145 180 } 146 181 return node 147 182 } ··· 188 223 isHighlightedPost: depth === 0, 189 224 hasMore: 190 225 direction === 'down' && !node.replies?.length && !!node.replyCount, 191 - showChildReplyLine: 192 - direction === 'up' || 193 - (direction === 'down' && !!node.replies?.length), 194 - showParentReplyLine: 195 - (direction === 'up' && !!node.parent) || 196 - (direction === 'down' && depth !== 1), 197 226 }, 198 227 } 199 228 } else if (AppBskyFeedDefs.isBlockedPost(node)) { ··· 296 325 depth: 0, 297 326 isHighlightedPost: true, 298 327 hasMore: false, 299 - showChildReplyLine: false, 300 - showParentReplyLine: false, 301 328 isParentLoading: !!node.record.reply, 302 329 isChildLoading: !!node.post.replyCount, 303 330 }, ··· 319 346 depth: 0, 320 347 isHighlightedPost: true, 321 348 hasMore: false, 322 - showChildReplyLine: false, 323 - showParentReplyLine: false, 324 349 isParentLoading: !!(post.record as AppBskyFeedPost.Record).reply, 325 350 isChildLoading: true, // assume yes (show the spinner) just in case 326 351 }, ··· 342 367 depth: 0, 343 368 isHighlightedPost: true, 344 369 hasMore: false, 345 - showChildReplyLine: false, 346 - showParentReplyLine: false, 347 370 isParentLoading: !!(record.value as AppBskyFeedPost.Record).reply, 348 371 isChildLoading: true, // not available, so assume yes (to show the spinner) 349 372 },
+132 -13
src/view/com/post-thread/PostThread.tsx
··· 10 10 import {isAndroid, isNative, isWeb} from '#/platform/detection' 11 11 import {useModerationOpts} from '#/state/preferences/moderation-opts' 12 12 import { 13 + fillThreadModerationCache, 13 14 sortThread, 14 15 ThreadBlocked, 16 + ThreadModerationCache, 15 17 ThreadNode, 16 18 ThreadNotFound, 17 19 ThreadPost, ··· 31 33 import {Text} from '../util/text/Text' 32 34 import {ViewHeader} from '../util/ViewHeader' 33 35 import {PostThreadItem} from './PostThreadItem' 36 + import {PostThreadShowHiddenReplies} from './PostThreadShowHiddenReplies' 34 37 35 38 // FlatList maintainVisibleContentPosition breaks if too many items 36 39 // are prepended. This seems to be an optimal number based on *shrug*. ··· 45 48 const TOP_COMPONENT = {_reactKey: '__top_component__'} 46 49 const REPLY_PROMPT = {_reactKey: '__reply__'} 47 50 const LOAD_MORE = {_reactKey: '__load_more__'} 51 + const SHOW_HIDDEN_REPLIES = {_reactKey: '__show_hidden_replies__'} 52 + const SHOW_MUTED_REPLIES = {_reactKey: '__show_muted_replies__'} 48 53 49 - type YieldedItem = ThreadPost | ThreadBlocked | ThreadNotFound 54 + enum HiddenRepliesState { 55 + Hide, 56 + Show, 57 + ShowAndOverridePostHider, 58 + } 59 + 60 + type YieldedItem = 61 + | ThreadPost 62 + | ThreadBlocked 63 + | ThreadNotFound 64 + | typeof SHOW_HIDDEN_REPLIES 65 + | typeof SHOW_MUTED_REPLIES 50 66 type RowItem = 51 67 | YieldedItem 52 68 // TODO: TS doesn't actually enforce it's one of these, it only enforces matching shape. ··· 79 95 const {isMobile, isTabletOrMobile} = useWebMediaQueries() 80 96 const initialNumToRender = useInitialNumToRender() 81 97 const {height: windowHeight} = useWindowDimensions() 98 + const [hiddenRepliesState, setHiddenRepliesState] = React.useState( 99 + HiddenRepliesState.Hide, 100 + ) 82 101 83 102 const {data: preferences} = usePreferencesQuery() 84 103 const { ··· 135 154 // On the web this is not necessary because we can synchronously adjust the scroll in onContentSizeChange instead. 136 155 const [deferParents, setDeferParents] = React.useState(isNative) 137 156 157 + const threadModerationCache = React.useMemo(() => { 158 + const cache: ThreadModerationCache = new WeakMap() 159 + if (thread && moderationOpts) { 160 + fillThreadModerationCache(cache, thread, moderationOpts) 161 + } 162 + return cache 163 + }, [thread, moderationOpts]) 164 + 138 165 const skeleton = React.useMemo(() => { 139 166 const threadViewPrefs = preferences?.threadViewPrefs 140 167 if (!threadViewPrefs || !thread) return null 141 168 142 169 return createThreadSkeleton( 143 - sortThread(thread, threadViewPrefs), 170 + sortThread(thread, threadViewPrefs, threadModerationCache), 144 171 hasSession, 145 172 treeView, 173 + threadModerationCache, 174 + hiddenRepliesState !== HiddenRepliesState.Hide, 146 175 ) 147 - }, [thread, preferences?.threadViewPrefs, hasSession, treeView]) 176 + }, [ 177 + thread, 178 + preferences?.threadViewPrefs, 179 + hasSession, 180 + treeView, 181 + threadModerationCache, 182 + hiddenRepliesState, 183 + ]) 148 184 149 185 const error = React.useMemo(() => { 150 186 if (AppBskyFeedDefs.isNotFoundPost(thread)) { ··· 301 337 {!isMobile && <ComposePrompt onPressCompose={onPressReply} />} 302 338 </View> 303 339 ) 340 + } else if (item === SHOW_HIDDEN_REPLIES) { 341 + return ( 342 + <PostThreadShowHiddenReplies 343 + type="hidden" 344 + onPress={() => 345 + setHiddenRepliesState(HiddenRepliesState.ShowAndOverridePostHider) 346 + } 347 + /> 348 + ) 349 + } else if (item === SHOW_MUTED_REPLIES) { 350 + return ( 351 + <PostThreadShowHiddenReplies 352 + type="muted" 353 + onPress={() => 354 + setHiddenRepliesState(HiddenRepliesState.ShowAndOverridePostHider) 355 + } 356 + /> 357 + ) 304 358 } else if (isThreadNotFound(item)) { 305 359 return ( 306 360 <View style={[pal.border, pal.viewLight, styles.itemContainer]}> ··· 321 375 const prev = isThreadPost(posts[index - 1]) 322 376 ? (posts[index - 1] as ThreadPost) 323 377 : undefined 324 - const next = isThreadPost(posts[index - 1]) 325 - ? (posts[index - 1] as ThreadPost) 378 + const next = isThreadPost(posts[index + 1]) 379 + ? (posts[index + 1] as ThreadPost) 326 380 : undefined 381 + const showChildReplyLine = (next?.ctx.depth || 0) > item.ctx.depth 382 + const showParentReplyLine = 383 + (item.ctx.depth < 0 && !!item.parent) || item.ctx.depth > 1 327 384 const hasUnrevealedParents = 328 385 index === 0 && 329 386 skeleton?.parents && ··· 335 392 <PostThreadItem 336 393 post={item.post} 337 394 record={item.record} 395 + moderation={threadModerationCache.get(item)} 338 396 treeView={treeView} 339 397 depth={item.ctx.depth} 340 398 prevPost={prev} 341 399 nextPost={next} 342 400 isHighlightedPost={item.ctx.isHighlightedPost} 343 401 hasMore={item.ctx.hasMore} 344 - showChildReplyLine={item.ctx.showChildReplyLine} 345 - showParentReplyLine={item.ctx.showParentReplyLine} 346 - hasPrecedingItem={ 347 - !!prev?.ctx.showChildReplyLine || !!hasUnrevealedParents 402 + showChildReplyLine={showChildReplyLine} 403 + showParentReplyLine={showParentReplyLine} 404 + hasPrecedingItem={showParentReplyLine || !!hasUnrevealedParents} 405 + overrideBlur={ 406 + hiddenRepliesState === 407 + HiddenRepliesState.ShowAndOverridePostHider && 408 + item.ctx.depth > 0 348 409 } 349 410 onPostReply={refetch} 350 411 /> ··· 368 429 deferParents, 369 430 treeView, 370 431 refetch, 432 + threadModerationCache, 433 + hiddenRepliesState, 434 + setHiddenRepliesState, 371 435 ], 372 436 ) 373 437 ··· 437 501 node: ThreadNode, 438 502 hasSession: boolean, 439 503 treeView: boolean, 504 + modCache: ThreadModerationCache, 505 + showHiddenReplies: boolean, 440 506 ): ThreadSkeletonParts | null { 441 507 if (!node) return null 442 508 443 509 return { 444 510 parents: Array.from(flattenThreadParents(node, hasSession)), 445 511 highlightedPost: node, 446 - replies: Array.from(flattenThreadReplies(node, hasSession, treeView)), 512 + replies: Array.from( 513 + flattenThreadReplies( 514 + node, 515 + hasSession, 516 + treeView, 517 + modCache, 518 + showHiddenReplies, 519 + ), 520 + ), 447 521 } 448 522 } 449 523 ··· 465 539 } 466 540 } 467 541 542 + // The enum is ordered to make them easy to merge 543 + enum HiddenReplyType { 544 + None = 0, 545 + Muted = 1, 546 + Hidden = 2, 547 + } 548 + 468 549 function* flattenThreadReplies( 469 550 node: ThreadNode, 470 551 hasSession: boolean, 471 552 treeView: boolean, 472 - ): Generator<YieldedItem, void> { 553 + modCache: ThreadModerationCache, 554 + showHiddenReplies: boolean, 555 + ): Generator<YieldedItem, HiddenReplyType> { 473 556 if (node.type === 'post') { 557 + // dont show pwi-opted-out posts to logged out users 474 558 if (!hasSession && hasPwiOptOut(node)) { 475 - return 559 + return HiddenReplyType.None 476 560 } 561 + 562 + // handle blurred items 563 + if (node.ctx.depth > 0) { 564 + const modui = modCache.get(node)?.ui('contentList') 565 + if (modui?.blur) { 566 + if (!showHiddenReplies || node.ctx.depth > 1) { 567 + if (modui.blurs[0].type === 'muted') { 568 + return HiddenReplyType.Muted 569 + } 570 + return HiddenReplyType.Hidden 571 + } 572 + } 573 + } 574 + 477 575 if (!node.ctx.isHighlightedPost) { 478 576 yield node 479 577 } 578 + 480 579 if (node.replies?.length) { 580 + let hiddenReplies = HiddenReplyType.None 481 581 for (const reply of node.replies) { 482 - yield* flattenThreadReplies(reply, hasSession, treeView) 582 + let hiddenReply = yield* flattenThreadReplies( 583 + reply, 584 + hasSession, 585 + treeView, 586 + modCache, 587 + showHiddenReplies, 588 + ) 589 + if (hiddenReply > hiddenReplies) { 590 + hiddenReplies = hiddenReply 591 + } 483 592 if (!treeView && !node.ctx.isHighlightedPost) { 484 593 break 485 594 } 486 595 } 596 + 597 + // show control to enable hidden replies 598 + if (node.ctx.depth === 0) { 599 + if (hiddenReplies === HiddenReplyType.Muted) { 600 + yield SHOW_MUTED_REPLIES 601 + } else if (hiddenReplies === HiddenReplyType.Hidden) { 602 + yield SHOW_HIDDEN_REPLIES 603 + } 604 + } 487 605 } 488 606 } else if (node.type === 'not-found') { 489 607 yield node 490 608 } else if (node.type === 'blocked') { 491 609 yield node 492 610 } 611 + return HiddenReplyType.None 493 612 } 494 613 495 614 function hasPwiOptOut(node: ThreadPost) {
+8 -8
src/view/com/post-thread/PostThreadItem.tsx
··· 11 11 import {msg, Plural, Trans} from '@lingui/macro' 12 12 import {useLingui} from '@lingui/react' 13 13 14 - import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped' 15 14 import {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow' 16 15 import {useLanguagePrefs} from '#/state/preferences' 17 16 import {useOpenLink} from '#/state/preferences/in-app-browser' 18 - import {useModerationOpts} from '#/state/preferences/moderation-opts' 19 17 import {ThreadPost} from '#/state/queries/post-thread' 20 18 import {useComposerControls} from '#/state/shell/composer' 21 19 import {MAX_POST_LINES} from 'lib/constants' ··· 50 48 export function PostThreadItem({ 51 49 post, 52 50 record, 51 + moderation, 53 52 treeView, 54 53 depth, 55 54 prevPost, ··· 59 58 showChildReplyLine, 60 59 showParentReplyLine, 61 60 hasPrecedingItem, 61 + overrideBlur, 62 62 onPostReply, 63 63 }: { 64 64 post: AppBskyFeedDefs.PostView 65 65 record: AppBskyFeedPost.Record 66 + moderation: ModerationDecision | undefined 66 67 treeView: boolean 67 68 depth: number 68 69 prevPost: ThreadPost | undefined ··· 72 73 showChildReplyLine?: boolean 73 74 showParentReplyLine?: boolean 74 75 hasPrecedingItem: boolean 76 + overrideBlur: boolean 75 77 onPostReply: () => void 76 78 }) { 77 - const moderationOpts = useModerationOpts() 78 79 const postShadowed = usePostShadow(post) 79 80 const richText = useMemo( 80 81 () => ··· 84 85 }), 85 86 [record], 86 87 ) 87 - const moderation = useMemo( 88 - () => 89 - post && moderationOpts ? moderatePost(post, moderationOpts) : undefined, 90 - [post, moderationOpts], 91 - ) 92 88 if (postShadowed === POST_TOMBSTONE) { 93 89 return <PostThreadItemDeleted /> 94 90 } ··· 110 106 showChildReplyLine={showChildReplyLine} 111 107 showParentReplyLine={showParentReplyLine} 112 108 hasPrecedingItem={hasPrecedingItem} 109 + overrideBlur={overrideBlur} 113 110 onPostReply={onPostReply} 114 111 /> 115 112 ) ··· 143 140 showChildReplyLine, 144 141 showParentReplyLine, 145 142 hasPrecedingItem, 143 + overrideBlur, 146 144 onPostReply, 147 145 }: { 148 146 post: Shadow<AppBskyFeedDefs.PostView> ··· 158 156 showChildReplyLine?: boolean 159 157 showParentReplyLine?: boolean 160 158 hasPrecedingItem: boolean 159 + overrideBlur: boolean 161 160 onPostReply: () => void 162 161 }): React.ReactNode => { 163 162 const pal = usePalette('default') ··· 394 393 <PostHider 395 394 testID={`postThreadItem-by-${post.author.handle}`} 396 395 href={postHref} 396 + disabled={overrideBlur} 397 397 style={[pal.view]} 398 398 modui={moderation.ui('contentList')} 399 399 iconSize={isThreadedChild ? 26 : 38}
+61
src/view/com/post-thread/PostThreadShowHiddenReplies.tsx
··· 1 + import * as React from 'react' 2 + import {View} from 'react-native' 3 + import {msg} from '@lingui/macro' 4 + import {useLingui} from '@lingui/react' 5 + 6 + import {atoms as a, useTheme} from '#/alf' 7 + import {Button} from '#/components/Button' 8 + import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash' 9 + import {Text} from '#/components/Typography' 10 + 11 + export function PostThreadShowHiddenReplies({ 12 + type, 13 + onPress, 14 + }: { 15 + type: 'hidden' | 'muted' 16 + onPress: () => void 17 + }) { 18 + const {_} = useLingui() 19 + const t = useTheme() 20 + const label = 21 + type === 'muted' ? _(msg`Show muted replies`) : _(msg`Show hidden replies`) 22 + 23 + return ( 24 + <Button onPress={onPress} label={label}> 25 + {({hovered, pressed}) => ( 26 + <View 27 + style={[ 28 + a.flex_1, 29 + a.flex_row, 30 + a.align_center, 31 + a.gap_sm, 32 + a.py_lg, 33 + a.px_xl, 34 + a.border_t, 35 + t.atoms.border_contrast_low, 36 + hovered || pressed ? t.atoms.bg_contrast_25 : t.atoms.bg, 37 + ]}> 38 + <View 39 + style={[ 40 + t.atoms.bg_contrast_25, 41 + a.align_center, 42 + a.justify_center, 43 + { 44 + width: 26, 45 + height: 26, 46 + borderRadius: 13, 47 + marginRight: 4, 48 + }, 49 + ]}> 50 + <EyeSlash size="sm" fill={t.atoms.text_contrast_medium.color} /> 51 + </View> 52 + <Text 53 + style={[t.atoms.text_contrast_medium, a.flex_1]} 54 + numberOfLines={1}> 55 + {label} 56 + </Text> 57 + </View> 58 + )} 59 + </Button> 60 + ) 61 + }
+1 -1
src/view/com/posts/FeedItem.tsx
··· 367 367 modui={moderation.ui('contentList')} 368 368 ignoreMute 369 369 childContainerStyle={styles.contentHiderChild}> 370 - <PostAlerts modui={moderation.ui('contentList')} style={[a.py_xs]} /> 370 + <PostAlerts modui={moderation.ui('contentList')} style={[a.pb_xs]} /> 371 371 {richText.text ? ( 372 372 <View style={styles.postTextContainer}> 373 373 <RichText
+3
src/view/screens/DebugMod.tsx
··· 813 813 814 814 function MockPostThreadItem({ 815 815 post, 816 + moderation, 816 817 reply, 817 818 }: { 818 819 post: AppBskyFeedDefs.PostView ··· 824 825 // @ts-ignore 825 826 post={post} 826 827 record={post.record as AppBskyFeedPost.Record} 828 + moderation={moderation} 827 829 depth={reply ? 1 : 0} 828 830 isHighlightedPost={!reply} 829 831 treeView={false} 830 832 prevPost={undefined} 831 833 nextPost={undefined} 832 834 hasPrecedingItem={false} 835 + overrideBlur={false} 833 836 onPostReply={() => {}} 834 837 /> 835 838 )