From b009a34a7ab641e90aeb45f8b1bd1ab6a953d398 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Wed, 14 Feb 2024 21:48:27 -0800 Subject: [PATCH] Refactor/rewrite the entire moderation-application system --- .../chevronBottom_stroke2_corner0_rounded.svg | 1 + .../chevronTop_stroke2_corner0_rounded.svg | 1 + .../icons/shield_stroke2_corner0_rounded.svg | 1 + src/alf/atoms.ts | 4 +- src/components/Button.tsx | 16 +- .../dialogs/ModerationDetails/index.tsx | 128 ++++ src/components/icons/Chevron.tsx | 8 + src/components/icons/Shield.tsx | 5 + src/lib/moderatePost_wrapped.ts | 13 +- src/lib/moderation.ts | 66 +- .../SuggestedAccountCard.tsx | 2 +- src/state/modals/index.tsx | 7 - src/state/queries/actor-autocomplete.ts | 4 +- src/state/queries/notifications/util.ts | 8 +- src/state/queries/post-feed.ts | 12 +- src/state/queries/preferences/index.ts | 19 +- src/state/queries/suggested-follows.ts | 3 +- src/state/shell/composer.tsx | 4 +- .../onboarding/RecommendedFollowsItem.tsx | 10 +- src/view/com/composer/Composer.tsx | 2 +- src/view/com/composer/ComposerReplyTo.tsx | 6 +- src/view/com/modals/Modal.tsx | 4 - src/view/com/modals/Modal.web.tsx | 3 - src/view/com/modals/ModerationDetails.tsx | 154 ---- src/view/com/notifications/FeedItem.tsx | 10 +- src/view/com/post-thread/PostThread.tsx | 11 +- src/view/com/post-thread/PostThreadItem.tsx | 45 +- src/view/com/post/Post.tsx | 25 +- src/view/com/posts/FeedItem.tsx | 38 +- src/view/com/profile/ProfileCard.tsx | 35 +- src/view/com/profile/ProfileHeader.tsx | 17 +- .../profile/ProfileHeaderSuggestedFollows.tsx | 4 +- src/view/com/util/moderation/ContentHider.tsx | 169 ++--- src/view/com/util/moderation/PostAlerts.tsx | 132 ++-- src/view/com/util/moderation/PostHider.tsx | 26 +- .../util/moderation/ProfileHeaderAlerts.tsx | 146 ++-- src/view/com/util/moderation/ScreenHider.tsx | 29 +- src/view/com/util/post-embeds/QuoteEmbed.tsx | 129 ++-- src/view/com/util/post-embeds/index.tsx | 104 ++- src/view/screens/DebugMod.tsx | 704 +++++++++--------- src/view/screens/Profile.tsx | 2 +- src/view/screens/Storybook/Buttons.tsx | 41 + src/view/shell/desktop/Search.tsx | 8 +- 43 files changed, 1094 insertions(+), 1062 deletions(-) create mode 100644 assets/icons/chevronBottom_stroke2_corner0_rounded.svg create mode 100644 assets/icons/chevronTop_stroke2_corner0_rounded.svg create mode 100644 assets/icons/shield_stroke2_corner0_rounded.svg create mode 100644 src/components/dialogs/ModerationDetails/index.tsx create mode 100644 src/components/icons/Shield.tsx delete mode 100644 src/view/com/modals/ModerationDetails.tsx diff --git a/assets/icons/chevronBottom_stroke2_corner0_rounded.svg b/assets/icons/chevronBottom_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..705c1c5139 --- /dev/null +++ b/assets/icons/chevronBottom_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/chevronTop_stroke2_corner0_rounded.svg b/assets/icons/chevronTop_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..da94ba911f --- /dev/null +++ b/assets/icons/chevronTop_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/shield_stroke2_corner0_rounded.svg b/assets/icons/shield_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..c4ef98e5ad --- /dev/null +++ b/assets/icons/shield_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/alf/atoms.ts b/src/alf/atoms.ts index 64b62f47c3..f75e8ffe0b 100644 --- a/src/alf/atoms.ts +++ b/src/alf/atoms.ts @@ -165,6 +165,9 @@ export const atoms = { /* * Text */ + text_left: { + textAlign: 'left', + }, text_center: { textAlign: 'center', }, @@ -242,7 +245,6 @@ export const atoms = { borderRightWidth: 1, }, - /* * Shadow */ diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 36a7207287..6a1e1da0a0 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -27,7 +27,7 @@ export type ButtonColor = | 'gradient_sunset' | 'gradient_nordic' | 'gradient_bonfire' -export type ButtonSize = 'small' | 'large' +export type ButtonSize = 'tiny' | 'small' | 'large' export type ButtonShape = 'round' | 'square' | 'default' export type VariantProps = { /** @@ -277,6 +277,8 @@ export function Button({ baseStyles.push({paddingVertical: 15}, a.px_2xl, a.rounded_sm, a.gap_md) } else if (size === 'small') { baseStyles.push({paddingVertical: 9}, a.px_lg, a.rounded_sm, a.gap_sm) + } else if (size === 'tiny') { + baseStyles.push({paddingVertical: 4}, a.px_sm, a.rounded_xs, a.gap_xs) } } else if (shape === 'round' || shape === 'square') { if (size === 'large') { @@ -287,12 +289,18 @@ export function Button({ } } else if (size === 'small') { baseStyles.push({height: 40, width: 40}) + } else if (size === 'tiny') { + baseStyles.push({height: 20, width: 20}) } if (shape === 'round') { baseStyles.push(a.rounded_full) } else if (shape === 'square') { - baseStyles.push(a.rounded_sm) + if (size === 'tiny') { + baseStyles.push(a.rounded_xs) + } else { + baseStyles.push(a.rounded_sm) + } } } @@ -493,6 +501,8 @@ export function useSharedButtonTextStyles() { if (size === 'large') { baseStyles.push(a.text_md, android({paddingBottom: 1})) + } else if (size === 'tiny') { + baseStyles.push(a.text_xs, android({paddingBottom: 1})) } else { baseStyles.push(a.text_sm, android({paddingBottom: 1})) } @@ -532,7 +542,7 @@ export function ButtonIcon({ }, ]}> diff --git a/src/components/dialogs/ModerationDetails/index.tsx b/src/components/dialogs/ModerationDetails/index.tsx new file mode 100644 index 0000000000..5a2074994f --- /dev/null +++ b/src/components/dialogs/ModerationDetails/index.tsx @@ -0,0 +1,128 @@ +import React from 'react' +import {View} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {ModerationCause} from '@atproto/api' + +import {atoms as a, useBreakpoints} from '#/alf' +import {Text} from '#/components/Typography' +import * as Dialog from '#/components/Dialog' +import {GlobalDialogProps} from '#/components/dialogs' +import {Button} from '#/components/Button' +import {InlineLink} from '#/components/Link' +import {useLabelStrings} from '#/lib/moderation/useLabelStrings' +import {listUriToHref} from '#/lib/strings/url-helpers' + +export interface ModerationDetailsDialogProps { + context: 'account' | 'content' + modcause: ModerationCause +} + +export function ModerationDetailsDialog({ + params, + cleanup, +}: GlobalDialogProps) { + const {_} = useLingui() + const labelStrings = useLabelStrings() + const control = Dialog.useDialogControl() + const {gtMobile} = useBreakpoints() + const {context, modcause} = params + + // REQUIRED CLEANUP + const onClose = React.useCallback(() => cleanup(), [cleanup]) + + let name + let description + if (!modcause) { + name = _(msg`Content Warning`) + description = _( + msg`Moderator has chosen to set a general warning on the content.`, + ) + } else if (modcause.type === 'blocking') { + if (modcause.source.type === 'list') { + const list = modcause.source.list + name = _(msg`User Blocked by List`) + description = ( + + This user is included in the{' '} + + {list.name} + {' '} + list which you have blocked. + + ) + } else { + name = _(msg`User Blocked`) + description = _( + msg`You have blocked this user. You cannot view their content.`, + ) + } + } else if (modcause.type === 'blocked-by') { + name = _(msg`User Blocks You`) + description = _( + msg`This user has blocked you. You cannot view their content.`, + ) + } else if (modcause.type === 'block-other') { + name = _(msg`Content Not Available`) + description = _( + msg`This content is not available because one of the users involved has blocked the other.`, + ) + } else if (modcause.type === 'muted') { + if (modcause.source.type === 'list') { + const list = modcause.source.list + name = _(msg`Account Muted by List`) + description = ( + + This user is included in the{' '} + + {list.name} + {' '} + list which you have muted. + + ) + } else { + name = _(msg`Account Muted`) + description = _(msg`You have muted this user.`) + } + } else if (modcause.type === 'label') { + if (modcause.labelDef.id in labelStrings) { + name = labelStrings[modcause.labelDef.id][context].name + description = labelStrings[modcause.labelDef.id][context].description + } else { + name = modcause.labelDef.id + description = _(msg`Labeled ${modcause.labelDef.id}`) + } + } else { + // should never happen + name = '' + description = '' + } + + return ( + + + + + + {name} + + + {description} + + + + + + + ) +} diff --git a/src/components/icons/Chevron.tsx b/src/components/icons/Chevron.tsx index b1a9deea0b..a04e6e0092 100644 --- a/src/components/icons/Chevron.tsx +++ b/src/components/icons/Chevron.tsx @@ -7,3 +7,11 @@ export const ChevronLeft_Stroke2_Corner0_Rounded = createSinglePathSVG({ export const ChevronRight_Stroke2_Corner0_Rounded = createSinglePathSVG({ path: 'M8.293 3.293a1 1 0 0 1 1.414 0l8 8a1 1 0 0 1 0 1.414l-8 8a1 1 0 0 1-1.414-1.414L15.586 12 8.293 4.707a1 1 0 0 1 0-1.414Z', }) + +export const ChevronTop_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M12 6a1 1 0 0 1 .707.293l8 8a1 1 0 0 1-1.414 1.414L12 8.414l-7.293 7.293a1 1 0 0 1-1.414-1.414l8-8A1 1 0 0 1 12 6Z', +}) + +export const ChevronBottom_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M3.293 8.293a1 1 0 0 1 1.414 0L12 15.586l7.293-7.293a1 1 0 1 1 1.414 1.414l-8 8a1 1 0 0 1-1.414 0l-8-8a1 1 0 0 1 0-1.414Z', +}) diff --git a/src/components/icons/Shield.tsx b/src/components/icons/Shield.tsx new file mode 100644 index 0000000000..5038d5c244 --- /dev/null +++ b/src/components/icons/Shield.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const Shield_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M11.675 2.054a1 1 0 0 1 .65 0l8 2.75A1 1 0 0 1 21 5.75v6.162c0 2.807-1.149 4.83-2.813 6.405-1.572 1.488-3.632 2.6-5.555 3.636l-.157.085a1 1 0 0 1-.95 0l-.157-.085c-1.923-1.037-3.983-2.148-5.556-3.636C4.15 16.742 3 14.719 3 11.912V5.75a1 1 0 0 1 .675-.946l8-2.75ZM5 6.464v5.448c0 2.166.851 3.687 2.188 4.952 1.276 1.209 2.964 2.158 4.812 3.157 1.848-1 3.536-1.948 4.813-3.157C18.148 15.6 19 14.078 19 11.912V6.464l-7-2.407-7 2.407Z', +}) diff --git a/src/lib/moderatePost_wrapped.ts b/src/lib/moderatePost_wrapped.ts index 2195b23044..e8ec4573f1 100644 --- a/src/lib/moderatePost_wrapped.ts +++ b/src/lib/moderatePost_wrapped.ts @@ -1,6 +1,6 @@ import { - AppBskyEmbedRecord, - AppBskyEmbedRecordWithMedia, + // AppBskyEmbedRecord, + // AppBskyEmbedRecordWithMedia, moderatePost, } from '@atproto/api' @@ -13,10 +13,11 @@ export function moderatePost_wrapped( subject: Parameters[0], opts: Options, ) { - const {hiddenPosts = [], ...options} = opts - const moderations = moderatePost(subject, options) + // const {hiddenPosts = [], ...options} = opts + const moderations = moderatePost(subject, opts) // options) - if (hiddenPosts.includes(subject.uri)) { + // TODO + /*if (hiddenPosts.includes(subject.uri)) { moderations.content.filter = true moderations.content.blur = true if (!moderations.content.cause) { @@ -52,7 +53,7 @@ export function moderatePost_wrapped( } } } - } + }*/ return moderations } diff --git a/src/lib/moderation.ts b/src/lib/moderation.ts index 9d7921c197..7f82a6c9f5 100644 --- a/src/lib/moderation.ts +++ b/src/lib/moderation.ts @@ -1,68 +1,4 @@ -import { - ModerationCause, - ProfileModeration, - PostModeration, - LABEL_GROUPS, - LabelGroupDefinition, -} from '@atproto/api' - -export function getProfileModerationCauses( - moderation: ProfileModeration, -): ModerationCause[] { - /* - Gather everything on profile and account that blurs or alerts - */ - return [ - moderation.decisions.profile.cause, - ...moderation.decisions.profile.additionalCauses, - moderation.decisions.account.cause, - ...moderation.decisions.account.additionalCauses, - ].filter(cause => { - if (!cause) { - return false - } - if (cause?.type === 'label') { - if ( - cause.labelDef.onwarn === 'blur' || - cause.labelDef.onwarn === 'alert' - ) { - return true - } else { - return false - } - } - return true - }) as ModerationCause[] -} - -export function isPostMediaBlurred( - decisions: PostModeration['decisions'], -): boolean { - return decisions.post.blurMedia -} - -export function isQuoteBlurred( - decisions: PostModeration['decisions'], -): boolean { - return ( - decisions.quote?.blur || - decisions.quote?.blurMedia || - decisions.quote?.filter || - decisions.quotedAccount?.blur || - decisions.quotedAccount?.filter || - false - ) -} - -export function isCauseALabelOnUri( - cause: ModerationCause | undefined, - uri: string, -): boolean { - if (cause?.type !== 'label') { - return false - } - return cause.label.uri === uri -} +import {ModerationCause, LABEL_GROUPS, LabelGroupDefinition} from '@atproto/api' export function getModerationCauseKey(cause: ModerationCause): string { const source = diff --git a/src/screens/Onboarding/StepSuggestedAccounts/SuggestedAccountCard.tsx b/src/screens/Onboarding/StepSuggestedAccounts/SuggestedAccountCard.tsx index 0670058920..7e4ea1f8b8 100644 --- a/src/screens/Onboarding/StepSuggestedAccounts/SuggestedAccountCard.tsx +++ b/src/screens/Onboarding/StepSuggestedAccounts/SuggestedAccountCard.tsx @@ -88,7 +88,7 @@ export function SuggestedAccountCard({ diff --git a/src/state/modals/index.tsx b/src/state/modals/index.tsx index 691add0050..0b73ff7e04 100644 --- a/src/state/modals/index.tsx +++ b/src/state/modals/index.tsx @@ -26,12 +26,6 @@ export interface EditProfileModal { onUpdate?: () => void } -export interface ModerationDetailsModal { - name: 'moderation-details' - context: 'account' | 'content' - moderation: ModerationUI -} - export type ReportModal = { name: 'report' } & ( @@ -203,7 +197,6 @@ export type Modal = | PostLanguagesSettingsModal // Moderation - | ModerationDetailsModal | ReportModal | AppealLabelModal diff --git a/src/state/queries/actor-autocomplete.ts b/src/state/queries/actor-autocomplete.ts index 329118a237..bffaea5531 100644 --- a/src/state/queries/actor-autocomplete.ts +++ b/src/state/queries/actor-autocomplete.ts @@ -107,8 +107,8 @@ function computeSuggestions( } } return items.filter(profile => { - const mod = moderateProfile(profile, moderationOpts) - return !mod.account.filter && mod.account.cause?.type !== 'muted' + const mod = moderateProfile(profile, moderationOpts).ui('profileList') + return !mod.filter // TODO && mod.account.cause?.type !== 'muted' }) } diff --git a/src/state/queries/notifications/util.ts b/src/state/queries/notifications/util.ts index 626d3e9118..06b639ee22 100644 --- a/src/state/queries/notifications/util.ts +++ b/src/state/queries/notifications/util.ts @@ -97,11 +97,7 @@ function shouldFilterNotif( return false } const profile = moderateProfile(notif.author, moderationOpts) - if ( - profile.account.filter || - profile.profile.filter || - notif.author.viewer?.muted - ) { + if (profile.ui('profileList').filter || notif.author.viewer?.muted) { return true } if ( @@ -111,7 +107,7 @@ function shouldFilterNotif( ) { // NOTE: the notification overlaps the post enough for this to work const post = moderatePost(notif, moderationOpts) - if (post.content.filter) { + if (post.ui('contentList').filter) { return true } } diff --git a/src/state/queries/post-feed.ts b/src/state/queries/post-feed.ts index 7eebce6eab..d7933486f0 100644 --- a/src/state/queries/post-feed.ts +++ b/src/state/queries/post-feed.ts @@ -1,6 +1,10 @@ import React, {useCallback, useEffect, useRef} from 'react' import {AppState} from 'react-native' -import {AppBskyFeedDefs, AppBskyFeedPost, PostModeration} from '@atproto/api' +import { + AppBskyFeedDefs, + AppBskyFeedPost, + ModerationDecision, +} from '@atproto/api' import { useInfiniteQuery, InfiniteData, @@ -62,7 +66,7 @@ export interface FeedPostSliceItem { post: AppBskyFeedDefs.PostView record: AppBskyFeedPost.Record reason?: AppBskyFeedDefs.ReasonRepost | ReasonFeedSource - moderation: PostModeration + moderation: ModerationDecision } export interface FeedPostSlice { @@ -227,7 +231,7 @@ export function usePostFeedQuery( // apply moderation filter for (let i = 0; i < slice.items.length; i++) { if ( - moderations[i]?.content.filter && + moderations[i]?.ui('contentList').filter && slice.items[i].post.author.did !== ignoreFilterFor ) { return undefined @@ -433,7 +437,7 @@ function assertSomePostsPassModeration(feed: AppBskyFeedDefs.FeedViewPost[]) { DEFAULT_LOGGED_OUT_PREFERENCES.moderationOpts, ) - if (!moderation.content.filter) { + if (!moderation.ui('contentList').filter) { // we have a sfw post somePostsPassModeration = true } diff --git a/src/state/queries/preferences/index.ts b/src/state/queries/preferences/index.ts index a3dedb5231..78506fe733 100644 --- a/src/state/queries/preferences/index.ts +++ b/src/state/queries/preferences/index.ts @@ -1,6 +1,10 @@ -import {useMemo} from 'react' +import {useMemo, createContext, useContext} from 'react' import {useQuery, useMutation, useQueryClient} from '@tanstack/react-query' -import {LabelPreference, BskyFeedViewPreference} from '@atproto/api' +import { + LabelPreference, + BskyFeedViewPreference, + ModerationOpts, +} from '@atproto/api' import {track} from '#/lib/analytics/analytics' import {getAge} from '#/lib/strings/time' @@ -63,10 +67,19 @@ export function usePreferencesQuery() { }) } +// used in the moderation state devtool +export const moderationOptsOverrideContext = createContext< + ModerationOpts | undefined +>(undefined) + export function useModerationOpts() { + const override = useContext(moderationOptsOverrideContext) const prefs = usePreferencesQuery() const hiddenPosts = useHiddenPosts() const opts = useMemo(() => { + if (override) { + return override + } if (!prefs.data) { return } @@ -75,7 +88,7 @@ export function useModerationOpts() { ...moderationOpts, hiddenPosts, } - }, [prefs.data, hiddenPosts]) + }, [override, prefs.data, hiddenPosts]) return opts } diff --git a/src/state/queries/suggested-follows.ts b/src/state/queries/suggested-follows.ts index 932226b75c..45b3ebb62f 100644 --- a/src/state/queries/suggested-follows.ts +++ b/src/state/queries/suggested-follows.ts @@ -46,7 +46,8 @@ export function useSuggestedFollowsQuery() { res.data.actors = res.data.actors .filter( - actor => !moderateProfile(actor, moderationOpts!).account.filter, + actor => + !moderateProfile(actor, moderationOpts!).ui('profileList').filter, ) .filter(actor => { const viewer = actor.viewer diff --git a/src/state/shell/composer.tsx b/src/state/shell/composer.tsx index 696a3c5ba1..c8392b825f 100644 --- a/src/state/shell/composer.tsx +++ b/src/state/shell/composer.tsx @@ -2,7 +2,7 @@ import React from 'react' import { AppBskyEmbedRecord, AppBskyRichtextFacet, - PostModeration, + ModerationDecision, } from '@atproto/api' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' @@ -16,7 +16,7 @@ export interface ComposerOptsPostRef { avatar?: string } embed?: AppBskyEmbedRecord.ViewRecord['embed'] - moderation?: PostModeration + moderation?: ModerationDecision } export interface ComposerOptsQuote { uri: string diff --git a/src/view/com/auth/onboarding/RecommendedFollowsItem.tsx b/src/view/com/auth/onboarding/RecommendedFollowsItem.tsx index 07001068cc..62889f0385 100644 --- a/src/view/com/auth/onboarding/RecommendedFollowsItem.tsx +++ b/src/view/com/auth/onboarding/RecommendedFollowsItem.tsx @@ -1,6 +1,6 @@ import React from 'react' import {View, StyleSheet, ActivityIndicator} from 'react-native' -import {ProfileModeration, AppBskyActorDefs} from '@atproto/api' +import {ModerationDecision, AppBskyActorDefs} from '@atproto/api' import {Button} from '#/view/com/util/forms/Button' import {usePalette} from 'lib/hooks/usePalette' import {sanitizeDisplayName} from 'lib/strings/display-names' @@ -18,7 +18,7 @@ import {logger} from '#/logger' type Props = { profile: AppBskyActorDefs.ProfileViewBasic - moderation: ProfileModeration + moderation: ModerationDecision onFollowStateChange: (props: { did: string following: boolean @@ -62,7 +62,7 @@ export function ProfileCard({ moderation, }: { profile: Shadow - moderation: ProfileModeration + moderation: ModerationDecision onFollowStateChange: (props: { did: string following: boolean @@ -110,7 +110,7 @@ export function ProfileCard({ @@ -121,7 +121,7 @@ export function ProfileCard({ lineHeight={1.2}> {sanitizeDisplayName( profile.displayName || sanitizeHandle(profile.handle), - moderation.profile, + moderation.ui('displayName'), )} diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 1ed6b98a55..f52b0890cc 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -39,7 +39,7 @@ import {usePalette} from 'lib/hooks/usePalette' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useExternalLinkFetch} from './useExternalLinkFetch' import {isWeb, isNative, isAndroid, isIOS} from 'platform/detection' -import QuoteEmbed from '../util/post-embeds/QuoteEmbed' +import {QuoteEmbed} from '../util/post-embeds/QuoteEmbed' import {GalleryModel} from 'state/models/media/gallery' import {Gallery} from './photos/Gallery' import {MAX_GRAPHEME_LENGTH} from 'lib/constants' diff --git a/src/view/com/composer/ComposerReplyTo.tsx b/src/view/com/composer/ComposerReplyTo.tsx index 39a1473a3b..4832bca02c 100644 --- a/src/view/com/composer/ComposerReplyTo.tsx +++ b/src/view/com/composer/ComposerReplyTo.tsx @@ -15,7 +15,7 @@ import {sanitizeDisplayName} from 'lib/strings/display-names' import {sanitizeHandle} from 'lib/strings/handles' import {UserAvatar} from 'view/com/util/UserAvatar' import {Text} from 'view/com/util/text/Text' -import QuoteEmbed from 'view/com/util/post-embeds/QuoteEmbed' +import {QuoteEmbed} from 'view/com/util/post-embeds/QuoteEmbed' export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) { const pal = usePalette('default') @@ -86,7 +86,7 @@ export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) { @@ -103,7 +103,7 @@ export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) { {replyTo.text} - {images && !replyTo.moderation?.embed.blur && ( + {images && !replyTo.moderation?.ui('contentMedia').blur && ( )} diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx index 8da91c75cf..8332e614ae 100644 --- a/src/view/com/modals/Modal.tsx +++ b/src/view/com/modals/Modal.tsx @@ -26,7 +26,6 @@ import * as AddAppPassword from './AddAppPasswords' import * as ContentFilteringSettingsModal from './ContentFilteringSettings' import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings' import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings' -import * as ModerationDetailsModal from './ModerationDetails' import * as BirthDateSettingsModal from './BirthDateSettings' import * as VerifyEmailModal from './VerifyEmail' import * as ChangeEmailModal from './ChangeEmail' @@ -127,9 +126,6 @@ export function ModalsContainer() { } else if (activeModal?.name === 'post-languages-settings') { snapPoints = PostLanguagesSettingsModal.snapPoints element = - } else if (activeModal?.name === 'moderation-details') { - snapPoints = ModerationDetailsModal.snapPoints - element = } else if (activeModal?.name === 'birth-date-settings') { snapPoints = BirthDateSettingsModal.snapPoints element = diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx index 97a60be913..4d94d74655 100644 --- a/src/view/com/modals/Modal.web.tsx +++ b/src/view/com/modals/Modal.web.tsx @@ -28,7 +28,6 @@ import * as AddAppPassword from './AddAppPasswords' import * as ContentFilteringSettingsModal from './ContentFilteringSettings' import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings' import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings' -import * as ModerationDetailsModal from './ModerationDetails' import * as BirthDateSettingsModal from './BirthDateSettings' import * as VerifyEmailModal from './VerifyEmail' import * as ChangeEmailModal from './ChangeEmail' @@ -121,8 +120,6 @@ function Modal({modal}: {modal: ModalIface}) { element = } else if (modal.name === 'edit-image') { element = - } else if (modal.name === 'moderation-details') { - element = } else if (modal.name === 'birth-date-settings') { element = } else if (modal.name === 'verify-email') { diff --git a/src/view/com/modals/ModerationDetails.tsx b/src/view/com/modals/ModerationDetails.tsx deleted file mode 100644 index f7ea598cce..0000000000 --- a/src/view/com/modals/ModerationDetails.tsx +++ /dev/null @@ -1,154 +0,0 @@ -import React from 'react' -import {StyleSheet, View} from 'react-native' -import {ModerationUI} from '@atproto/api' -import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -import {s} from 'lib/styles' -import {Text} from '../util/text/Text' -import {TextLink} from '../util/Link' -import {usePalette} from 'lib/hooks/usePalette' -import {isWeb} from 'platform/detection' -import {listUriToHref} from 'lib/strings/url-helpers' -import {Button} from '../util/forms/Button' -import {useModalControls} from '#/state/modals' -import {useLingui} from '@lingui/react' -import {Trans, msg} from '@lingui/macro' -import {useLabelStrings} from '#/lib/moderation/useLabelStrings' - -export const snapPoints = [300] - -export function Component({ - context, - moderation, -}: { - context: 'account' | 'content' - moderation: ModerationUI -}) { - const {closeModal} = useModalControls() - const {isMobile} = useWebMediaQueries() - const pal = usePalette('default') - const {_} = useLingui() - const labelStrings = useLabelStrings() - - let name - let description - if (!moderation.cause) { - name = _(msg`Content Warning`) - description = _( - msg`Moderator has chosen to set a general warning on the content.`, - ) - } else if (moderation.cause.type === 'blocking') { - if (moderation.cause.source.type === 'list') { - const list = moderation.cause.source.list - name = _(msg`User Blocked by List`) - description = ( - - This user is included in the{' '} - {' '} - list which you have blocked. - - ) - } else { - name = _(msg`User Blocked`) - description = _( - msg`You have blocked this user. You cannot view their content.`, - ) - } - } else if (moderation.cause.type === 'blocked-by') { - name = _(msg`User Blocks You`) - description = _( - msg`This user has blocked you. You cannot view their content.`, - ) - } else if (moderation.cause.type === 'block-other') { - name = _(msg`Content Not Available`) - description = _( - msg`This content is not available because one of the users involved has blocked the other.`, - ) - } else if (moderation.cause.type === 'muted') { - if (moderation.cause.source.type === 'list') { - const list = moderation.cause.source.list - name = _(msg`Account Muted by List`) - description = ( - - This user is included in the{' '} - {' '} - list which you have muted. - - ) - } else { - name = _(msg`Account Muted`) - description = _(msg`You have muted this user.`) - } - } else if (moderation.cause.type === 'label') { - if (moderation.cause.labelDef.id in labelStrings) { - name = labelStrings[moderation.cause.labelDef.id][context].name - description = - labelStrings[moderation.cause.labelDef.id][context].description - } else { - name = moderation.cause.labelDef.id - description = _(msg`Labeled ${moderation.cause.labelDef.id}`) - } - } else { - // should never happen - name = '' - description = '' - } - - return ( - - - {name} - - - {description} - - - - - ) -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - }, - title: { - textAlign: 'center', - fontWeight: 'bold', - marginBottom: 12, - }, - description: { - textAlign: 'center', - }, - btn: { - paddingVertical: 14, - marginTop: isWeb ? 40 : 0, - marginBottom: isWeb ? 0 : 40, - }, -}) diff --git a/src/view/com/notifications/FeedItem.tsx b/src/view/com/notifications/FeedItem.tsx index f037097dfc..7567b264ec 100644 --- a/src/view/com/notifications/FeedItem.tsx +++ b/src/view/com/notifications/FeedItem.tsx @@ -11,7 +11,7 @@ import { AppBskyFeedDefs, AppBskyFeedPost, ModerationOpts, - ProfileModeration, + ModerationDecision, moderateProfile, AppBskyEmbedRecordWithMedia, } from '@atproto/api' @@ -54,7 +54,7 @@ interface Author { handle: string displayName?: string avatar?: string - moderation: ProfileModeration + moderation: ModerationDecision } let FeedItem = ({ @@ -336,7 +336,7 @@ function CondensedAuthorsList({ did={authors[0].did} handle={authors[0].handle} avatar={authors[0].avatar} - moderation={authors[0].moderation.avatar} + moderation={authors[0].moderation.ui('avatar')} /> ) @@ -354,7 +354,7 @@ function CondensedAuthorsList({ ))} @@ -412,7 +412,7 @@ function ExpandedAuthorsList({ diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx index ef3d7e2b6a..a8b2ed6677 100644 --- a/src/view/com/post-thread/PostThread.tsx +++ b/src/view/com/post-thread/PostThread.tsx @@ -90,11 +90,12 @@ export function PostThread({ ? moderatePost(rootPost, moderationOpts) : undefined - const cause = mod?.content.cause - - return cause - ? cause.type === 'label' && cause.labelDef.id === '!no-unauthenticated' - : false + return !!mod + ?.ui('contentList') + .blurs.find( + cause => + cause.type === 'label' && cause.labelDef.id === '!no-unauthenticated', + ) }, [rootPost, moderationOpts]) useSetTitle( diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index 17528b14ec..3659c63842 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -5,7 +5,7 @@ import { AppBskyFeedDefs, AppBskyFeedPost, RichText as RichTextAPI, - PostModeration, + ModerationDecision, } from '@atproto/api' import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' @@ -19,7 +19,6 @@ import {niceDate} from 'lib/strings/time' import {sanitizeDisplayName} from 'lib/strings/display-names' import {sanitizeHandle} from 'lib/strings/handles' import {countLines, pluralize} from 'lib/strings/helpers' -import {isEmbedByEmbedder} from 'lib/embeds' import {getTranslatorLink, isPostInLanguage} from '../../../locale/helpers' import {PostMeta} from '../util/PostMeta' import {PostEmbeds} from '../util/post-embeds' @@ -144,7 +143,7 @@ let PostThreadItemLoaded = ({ post: Shadow record: AppBskyFeedPost.Record richText: RichTextAPI - moderation: PostModeration + moderation: ModerationDecision treeView: boolean depth: number prevPost: ThreadPost | undefined @@ -256,7 +255,7 @@ let PostThreadItemLoaded = ({ did={post.author.did} handle={post.author.handle} avatar={post.author.avatar} - moderation={moderation.avatar} + moderation={moderation.ui('avatar')} /> @@ -313,12 +312,12 @@ let PostThreadItemLoaded = ({ @@ -338,18 +337,7 @@ let PostThreadItemLoaded = ({ ) : undefined} {post.embed && ( - - - + )} {showChildReplyLine && ( @@ -507,14 +495,14 @@ let PostThreadItemLoaded = ({ timestamp={post.indexedAt} postHref={postHref} showAvatar={isThreadedChild} - avatarModeration={moderation.avatar} + avatarModeration={moderation.ui('avatar')} avatarSize={28} displayNameType="md-bold" displayNameStyle={isThreadedChild && s.ml2} style={isThreadedChild && s.mb2} /> {richText?.text ? ( @@ -537,18 +525,7 @@ let PostThreadItemLoaded = ({ /> ) : undefined} {post.embed && ( - - - + )} record: AppBskyFeedPost.Record richText: RichTextAPI - moderation: PostModeration + moderation: ModerationDecision showReplyLine?: boolean style?: StyleProp }) { @@ -141,7 +141,7 @@ function PostInner({ did={post.author.did} handle={post.author.handle} avatar={post.author.avatar} - moderation={moderation.avatar} + moderation={moderation.ui('avatar')} /> @@ -176,10 +176,13 @@ function PostInner({ )} - + {richText.text ? ( ) : undefined} {post.embed ? ( - - - + ) : null} + cause?.type === 'label' && + cause.label.src !== currentAccount?.did && + cause.label.uri === post.uri, + ) const replyAuthorDid = useMemo(() => { if (!record?.reply) { @@ -247,7 +249,7 @@ let FeedItemInner = ({ did={post.author.did} handle={post.author.handle} avatar={post.author.avatar} - moderation={moderation.avatar} + moderation={moderation.ui('avatar')} /> {isThreadParent && ( - + {richText.text ? ( ) : undefined} {postEmbed ? ( - - - + ) : null} ) diff --git a/src/view/com/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx index d12b41496e..0ba0655407 100644 --- a/src/view/com/profile/ProfileCard.tsx +++ b/src/view/com/profile/ProfileCard.tsx @@ -4,7 +4,7 @@ import { AppBskyActorDefs, moderateProfile, ModerationCause, - ProfileModeration, + ModerationDecision, } from '@atproto/api' import {Link} from '../util/Link' import {Text} from '../util/text/Text' @@ -15,7 +15,7 @@ import {FollowButton} from './FollowButton' import {sanitizeDisplayName} from 'lib/strings/display-names' import {sanitizeHandle} from 'lib/strings/handles' import {makeProfileLink} from 'lib/routes/links' -import {getProfileModerationCauses, getModerationCauseKey} from 'lib/moderation' +import {getModerationCauseKey} from 'lib/moderation' import {Shadow} from '#/state/cache/types' import {useModerationOpts} from '#/state/queries/preferences' import {useProfileShadow} from '#/state/cache/profile-shadow' @@ -51,11 +51,8 @@ export function ProfileCard({ return null } const moderation = moderateProfile(profile, moderationOpts) - if ( - !noModFilter && - moderation.account.filter && - moderation.account.cause?.type !== 'muted' - ) { + const modui = moderation.ui('profileList') + if (!noModFilter && modui.filter /* TODO && modui.type !== 'muted'*/) { return null } @@ -78,7 +75,7 @@ export function ProfileCard({ @@ -89,7 +86,7 @@ export function ProfileCard({ lineHeight={1.2}> {sanitizeDisplayName( profile.displayName || sanitizeHandle(profile.handle), - moderation.profile, + moderation.ui('displayName'), )} @@ -122,12 +119,12 @@ export function ProfileCardPills({ moderation, }: { followedBy: boolean - moderation: ProfileModeration + moderation: ModerationDecision }) { const pal = usePalette('default') - const causes = getProfileModerationCauses(moderation) - if (!followedBy && !causes.length) { + const informs = moderation.ui('profileList').informs + if (!followedBy && !informs.length) { return null } @@ -140,10 +137,10 @@ export function ProfileCardPills({ )} - {causes.map(cause => ( + {informs.map(inform => ( ))} @@ -183,7 +180,7 @@ function FollowersList({ f, mod: moderateProfile(f, moderationOpts), })) - .filter(({mod}) => !mod.account.filter) + .filter(({mod}) => !mod.ui('profileList').filter) }, [followers, moderationOpts]) if (!followersWithMods?.length) { @@ -205,7 +202,11 @@ function FollowersList({ {followersWithMods.slice(0, 3).map(({f, mod}) => ( - + ))} diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx index 34160ec977..41a3503a8d 100644 --- a/src/view/com/profile/ProfileHeader.tsx +++ b/src/view/com/profile/ProfileHeader.tsx @@ -134,10 +134,8 @@ let ProfileHeader = ({ }, [navigation]) const onPressAvi = React.useCallback(() => { - if ( - profile.avatar && - !(moderation.avatar.blur && moderation.avatar.noOverride) - ) { + const modui = moderation.ui('avatar') + if (profile.avatar && !(modui.blur && modui.noOverride)) { openLightbox(new ProfileImageLightbox(profile)) } }, [openLightbox, profile, moderation]) @@ -407,7 +405,10 @@ let ProfileHeader = ({ style={{borderRadius: 0}} /> ) : ( - + )} @@ -538,7 +539,7 @@ let ProfileHeader = ({ style={[pal.text, styles.title]}> {sanitizeDisplayName( profile.displayName || sanitizeHandle(profile.handle), - moderation.profile, + moderation.ui('displayName'), )} @@ -611,7 +612,7 @@ let ProfileHeader = ({ - {descriptionRT && !moderation.profile.blur ? ( + {descriptionRT && !moderation.ui('profileView').blur ? ( diff --git a/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx b/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx index 6edc61fcf8..d3dfcd424d 100644 --- a/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx +++ b/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx @@ -214,7 +214,7 @@ function SuggestedFollow({ @@ -224,7 +224,7 @@ function SuggestedFollow({ numberOfLines={1}> {sanitizeDisplayName( profile.displayName || sanitizeHandle(profile.handle), - moderation.profile, + moderation.ui('displayName'), )} childContainerStyle?: StyleProp }>) { - const pal = usePalette('default') + const t = useTheme() const {_} = useLingui() const [override, setOverride] = React.useState(false) - const {openModal} = useModalControls() - const desc = useModerationCauseDescription(moderation.cause, 'content') + const {gtMobile} = useBreakpoints() + const openDialog = useOpenGlobalDialog() + + const blur = modui?.blurs[0] + const desc = useModerationCauseDescription(blur, 'content') if ( - !moderation.blur || - (ignoreMute && moderation.cause?.type === 'muted') || - shouldIgnoreQuote(moderationDecisions, ignoreQuoteDecisions) + !blur + // || (ignoreMute && moderation.cause?.type === 'muted') TODO ) { return ( @@ -47,83 +47,74 @@ export function ContentHider({ ) } - const isMute = moderation.cause?.type === 'muted' return ( - - { - if (!moderation.noOverride) { - setOverride(v => !v) - } else { - openModal({ - name: 'moderation-details', - context: 'content', - moderation, - }) - } - }} - accessibilityRole="button" - accessibilityHint={ - override ? _(msg`Hide the content`) : _(msg`Show the content`) - } - accessibilityLabel="" - style={[ - styles.cover, - moderation.noOverride - ? {borderWidth: 1, borderColor: pal.colors.borderDark} - : pal.viewLight, - ]}> - + + + {blur.type === 'label' && !override && ( + + )} + {override && {children}} ) } -function shouldIgnoreQuote( - decisions: PostModeration['decisions'] | undefined, - ignore: boolean | undefined, -): boolean { - if (!decisions || !ignore) { - return false - } - return !isPostMediaBlurred(decisions) -} - const styles = StyleSheet.create({ outer: { overflow: 'hidden', diff --git a/src/view/com/util/moderation/PostAlerts.tsx b/src/view/com/util/moderation/PostAlerts.tsx index f65c2b1ca3..50a9de296c 100644 --- a/src/view/com/util/moderation/PostAlerts.tsx +++ b/src/view/com/util/moderation/PostAlerts.tsx @@ -1,67 +1,99 @@ import React from 'react' -import {Pressable, StyleProp, StyleSheet, ViewStyle} from 'react-native' -import {ModerationUI} from '@atproto/api' +import {StyleProp, View, ViewStyle} from 'react-native' +import {ModerationUI, ModerationCause} from '@atproto/api' import {Text} from '../text/Text' -import {usePalette} from 'lib/hooks/usePalette' -import {ShieldExclamation} from 'lib/icons' -import {Trans, msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {useModalControls} from '#/state/modals' +import {Trans} from '@lingui/macro' import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription' +import {getModerationCauseKey} from '#/lib/moderation' + +import {atoms as a, useTheme} from '#/alf' +import {Button, ButtonText, ButtonIcon} from '#/components/Button' +import {Shield_Stroke2_Corner0_Rounded as Shield} from '#/components/icons/Shield' +import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' +import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash' +import {ModerationDetailsDialog} from '#/components/dialogs/ModerationDetails' +import {useOpenGlobalDialog} from '#/components/dialogs' export function PostAlerts({ - moderation, + modui, style, }: { - moderation: ModerationUI + modui: ModerationUI includeMute?: boolean style?: StyleProp }) { - const pal = usePalette('default') - const {_} = useLingui() - const {openModal} = useModalControls() - const desc = useModerationCauseDescription(moderation.cause, 'content') - - const shouldAlert = !!moderation.cause && moderation.alert - if (!shouldAlert) { + if (!modui.alert && !modui.inform) { return null } return ( - { - openModal({ - name: 'moderation-details', - context: 'content', - moderation, - }) - }} - accessibilityRole="button" - accessibilityLabel={_(msg`Learn more about this warning`)} - accessibilityHint="" - style={[styles.container, pal.viewLight, style]}> - - - {desc.name}{' '} - - Learn More - - - + + {modui.inform && ( + + {modui.informs.map(cause => ( + + ))} + + )} + {modui.alerts.map(cause => ( + + ))} + ) } -const styles = StyleSheet.create({ - container: { - flexDirection: 'row', - alignItems: 'center', - gap: 4, - paddingVertical: 8, - paddingLeft: 14, - paddingHorizontal: 16, - borderRadius: 8, - }, - learnMoreBtn: { - marginLeft: 'auto', - }, -}) +function PostInform({cause}: {cause: ModerationCause}) { + const openDialog = useOpenGlobalDialog() + const desc = useModerationCauseDescription(cause, 'content') + + return ( + + ) +} + +function PostAlert({cause}: {cause: ModerationCause}) { + const t = useTheme() + const openDialog = useOpenGlobalDialog() + const desc = useModerationCauseDescription(cause, 'content') + + return ( + + ) +} diff --git a/src/view/com/util/moderation/PostHider.tsx b/src/view/com/util/moderation/PostHider.tsx index 3d2ad49258..d74b876310 100644 --- a/src/view/com/util/moderation/PostHider.tsx +++ b/src/view/com/util/moderation/PostHider.tsx @@ -9,19 +9,21 @@ import {addStyle} from 'lib/styles' import {ShieldExclamation} from 'lib/icons' import {useLingui} from '@lingui/react' import {Trans, msg} from '@lingui/macro' -import {useModalControls} from '#/state/modals' import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription' +import {ModerationDetailsDialog} from '#/components/dialogs/ModerationDetails' +import {useOpenGlobalDialog} from '#/components/dialogs' + interface Props extends ComponentProps { iconSize: number iconStyles: StyleProp - moderation: ModerationUI + modui: ModerationUI } export function PostHider({ testID, href, - moderation, + modui, style, children, iconSize, @@ -31,10 +33,11 @@ export function PostHider({ const pal = usePalette('default') const {_} = useLingui() const [override, setOverride] = React.useState(false) - const {openModal} = useModalControls() - const desc = useModerationCauseDescription(moderation.cause, 'content') + const openDialog = useOpenGlobalDialog() + const blur = modui.blurs[0] + const desc = useModerationCauseDescription(blur, 'content') - if (!moderation.blur) { + if (!blur) { return ( { - if (!moderation.noOverride) { + if (!modui.noOverride) { setOverride(v => !v) } }} @@ -68,10 +71,9 @@ export function PostHider({ ]}> { - openModal({ - name: 'moderation-details', + openDialog(ModerationDetailsDialog, { context: 'content', - moderation, + modcause: blur, }) }} accessibilityRole="button" @@ -103,7 +105,7 @@ export function PostHider({ {desc.name} - {!moderation.noOverride && ( + {!modui.noOverride && ( {override ? Hide : Show} diff --git a/src/view/com/util/moderation/ProfileHeaderAlerts.tsx b/src/view/com/util/moderation/ProfileHeaderAlerts.tsx index 25f33621d3..2381538ec2 100644 --- a/src/view/com/util/moderation/ProfileHeaderAlerts.tsx +++ b/src/view/com/util/moderation/ProfileHeaderAlerts.tsx @@ -1,101 +1,99 @@ import React from 'react' -import {Pressable, StyleProp, StyleSheet, View, ViewStyle} from 'react-native' -import {ModerationCause, ProfileModeration} from '@atproto/api' +import {StyleProp, View, ViewStyle} from 'react-native' +import {ModerationCause, ModerationDecision} from '@atproto/api' import {Text} from '../text/Text' -import {usePalette} from 'lib/hooks/usePalette' -import {ShieldExclamation} from 'lib/icons' -import {getModerationCauseKey, getProfileModerationCauses} from 'lib/moderation' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {useModalControls} from '#/state/modals' +import {getModerationCauseKey} from 'lib/moderation' +import {Trans} from '@lingui/macro' import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription' +import {atoms as a, useTheme} from '#/alf' +import {Button, ButtonText, ButtonIcon} from '#/components/Button' +import {Shield_Stroke2_Corner0_Rounded as Shield} from '#/components/icons/Shield' +import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' +import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash' +import {ModerationDetailsDialog} from '#/components/dialogs/ModerationDetails' +import {useOpenGlobalDialog} from '#/components/dialogs' + export function ProfileHeaderAlerts({ moderation, style, }: { - moderation: ProfileModeration + moderation: ModerationDecision style?: StyleProp }) { - const causes = getProfileModerationCauses(moderation) - if (!causes.length) { + const modui = moderation.ui('profileView') + if (!modui.alert && !modui.inform) { return null } return ( - - {causes.map(cause => ( - + + {modui.inform && ( + + {modui.informs.map(cause => ( + + ))} + + )} + {modui.alerts.map(cause => ( + ))} ) } -function ProfileHeaderAlert({ - cause, - style, -}: { - cause: ModerationCause - style?: StyleProp -}) { - const pal = usePalette('default') - const {_} = useLingui() - const {openModal} = useModalControls() - const desc = useModerationCauseDescription(cause, 'account') +function ProfileInform({cause}: {cause: ModerationCause}) { + const openDialog = useOpenGlobalDialog() + const desc = useModerationCauseDescription(cause, 'content') - const isMute = cause.type === 'muted' return ( - { - openModal({ - name: 'moderation-details', + openDialog(ModerationDetailsDialog, { context: 'content', - moderation: {cause}, + modcause: cause, }) - }} - accessibilityRole="button" - accessibilityLabel={_(msg`Learn more about this warning`)} - accessibilityHint="" - style={[styles.container, pal.viewLight, style]}> - {isMute ? ( - - ) : ( - - )} - - {desc.name} - - - Learn More - - + }}> + {' '} + {desc.name} + ) } -const styles = StyleSheet.create({ - grid: { - gap: 4, - }, - container: { - flexDirection: 'row', - alignItems: 'center', - gap: 8, - paddingVertical: 12, - paddingHorizontal: 16, - borderRadius: 8, - }, - learnMoreBtn: { - marginLeft: 'auto', - }, -}) +function ProfileAlert({cause}: {cause: ModerationCause}) { + const t = useTheme() + const openDialog = useOpenGlobalDialog() + const desc = useModerationCauseDescription(cause, 'content') + + return ( + + ) +} diff --git a/src/view/com/util/moderation/ScreenHider.tsx b/src/view/com/util/moderation/ScreenHider.tsx index 517db287aa..b9ac96eb2b 100644 --- a/src/view/com/util/moderation/ScreenHider.tsx +++ b/src/view/com/util/moderation/ScreenHider.tsx @@ -19,22 +19,24 @@ import {Text} from '../text/Text' import {Button} from '../forms/Button' import {Trans, msg} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {useModalControls} from '#/state/modals' import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription' import {s} from '#/lib/styles' import {CenteredView} from '../Views' +import {ModerationDetailsDialog} from '#/components/dialogs/ModerationDetails' +import {useOpenGlobalDialog} from '#/components/dialogs' + export function ScreenHider({ testID, screenDescription, - moderation, + modui, style, containerStyle, children, }: React.PropsWithChildren<{ testID?: string screenDescription: string - moderation: ModerationUI + modui: ModerationUI style?: StyleProp containerStyle?: StyleProp }>) { @@ -44,10 +46,11 @@ export function ScreenHider({ const [override, setOverride] = React.useState(false) const navigation = useNavigation() const {isMobile} = useWebMediaQueries() - const {openModal} = useModalControls() - const desc = useModerationCauseDescription(moderation.cause, 'account') + const openDialog = useOpenGlobalDialog() + const blur = modui.blurs[0] + const desc = useModerationCauseDescription(blur, 'content') - if (!moderation.blur || override) { + if (!blur || override) { return ( {children} @@ -55,9 +58,10 @@ export function ScreenHider({ ) } - const isNoPwi = - moderation.cause?.type === 'label' && - moderation.cause?.labelDef.id === '!no-unauthenticated' + const isNoPwi = !!modui.blurs.find( + cause => + cause.type === 'label' && cause.labelDef.id === '!no-unauthenticated', + ) return ( { - openModal({ - name: 'moderation-details', + openDialog(ModerationDetailsDialog, { context: 'account', - moderation, + modcause: blur, }) }} accessibilityRole="button" @@ -123,7 +126,7 @@ export function ScreenHider({ Go back - {!moderation.noOverride && ( + {!modui.noOverride && ( + {show && children} + + + ) } -function MockPost({ +function DataView({label, data}: {label: string; data: any}) { + return ( + + + {JSON.stringify(data, null, 2)} + + + ) +} + +function ModerationUIView({ + mod, label, - context, +}: { + mod: ModerationDecision + label: string +}) { + return ( + + + {[ + 'profileList', + 'profileView', + 'avatar', + 'banner', + 'displayName', + 'contentList', + 'contentView', + 'contentMedia', + ].map(key => { + const ui = mod.ui(key as keyof ModerationBehavior) + return ( + + {key} + + + + + + + ) + })} + + + ) +} + +function Spacer() { + return +} + +function MockPostFeedItem({ post, moderation, }: { - label: string - context: 'feed' | 'view' | 'reply' post: AppBskyFeedDefs.PostView - moderation: PostModeration + moderation: ModerationDecision }) { const t = useTheme() - - if (context === 'feed' && moderation.content.filter) { + if (moderation.ui('contentList').filter) { return (

Filtered from the feed

) } - return ( - - {' '} - - - - - -

Bob Robertson

-

- @bob.bsky.social · 5m -

-
+ + ) +} - - -

- This is the body of the post. It's where the text goes. You get - the idea. -

- - - - -

Bob Robertson

-

- @bob.bsky.social · 5m -

-
- -

Embedded content

-
-
-
-
-
-
+function MockPostThreadItem({ + post, + reply, +}: { + post: AppBskyFeedDefs.PostView + moderation: ModerationDecision + reply?: boolean +}) { + return ( + {}} + /> ) } function MockAccountCard({ - label, profile, moderation, }: { - label: string profile: AppBskyActorDefs.ProfileViewBasic - moderation: ProfileModeration + moderation: ModerationDecision }) { const t = useTheme() - if (moderation.account.filter || moderation.profile.filter) { + if (moderation.ui('profileList').filter) { return (

Filtered from the listing @@ -446,104 +542,36 @@ function MockAccountCard({ ) } - return ( - - - -

- {sanitizeDisplayName('Bob Robertson', moderation.profile)}{' '} -

-

@bob.bsky.social

-

Thought leader or something.

- -
- - ) + return } function MockAccountScreen({ - label, profile, moderation, moderationOpts, }: { - label: string profile: AppBskyActorDefs.ProfileViewBasic - moderation: ProfileModeration + moderation: ModerationDecision moderationOpts: ModerationOpts }) { const t = useTheme() return ( - + + modui={moderation.ui('profileView')}> - - {/* - - - - - -

Bob Robertson

-

@bob.bsky.social

-

Thought leader or something.

-
- */}
) } -function ModerationUIView({mod, label}: {mod: ModerationUI; label: string}) { - return ( - -

{label}:

- - - - -
- ) -} - function Flag({v, label}: {v: boolean | undefined; label: string}) { const t = useTheme() return ( @@ -556,12 +584,12 @@ function Flag({v, label}: {v: boolean | undefined; label: string}) { a.border, t.atoms.border_contrast_medium, { - backgroundColor: v ? t.palette.black : t.palette.white, + backgroundColor: t.palette.contrast_25, width: 14, height: 14, }, ]}> - {v && } + {v && }

{label}

diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx index 64e067593b..abafff8148 100644 --- a/src/view/screens/Profile.tsx +++ b/src/view/screens/Profile.tsx @@ -298,7 +298,7 @@ function ProfileScreenLoaded({ testID="profileView" style={styles.container} screenDescription="profile" - moderation={moderation.account}> + modui={moderation.ui('profileView')}> Link out + + @@ -148,6 +157,14 @@ export function Buttons() { label="Link out"> + + @@ -183,6 +208,14 @@ export function Buttons() { label="Link out"> + + ) diff --git a/src/view/shell/desktop/Search.tsx b/src/view/shell/desktop/Search.tsx index 4a94837338..8933324eed 100644 --- a/src/view/shell/desktop/Search.tsx +++ b/src/view/shell/desktop/Search.tsx @@ -11,7 +11,7 @@ import {useNavigation, StackActions} from '@react-navigation/native' import { AppBskyActorDefs, moderateProfile, - ProfileModeration, + ModerationDecision, } from '@atproto/api' import {Trans, msg} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -86,7 +86,7 @@ export function SearchProfileCard({ moderation, }: { profile: AppBskyActorDefs.ProfileViewBasic - moderation: ProfileModeration + moderation: ModerationDecision }) { const pal = usePalette('default') @@ -111,7 +111,7 @@ export function SearchProfileCard({ {sanitizeDisplayName( profile.displayName || sanitizeHandle(profile.handle), - moderation.profile, + moderation.ui('displayName'), )}