From 56fa8aca31b1b7bbe5aa72b89c2df345d66da2e8 Mon Sep 17 00:00:00 2001 From: Austin McKinley Date: Tue, 7 Jul 2026 13:03:33 -0700 Subject: [PATCH] Collapse label appeal flow into ModerationDetailsDialog Labels on your own posts previously rendered twice: once as normal label chips and again as an ominous "1 content label" pill whose only purpose was reaching the appeal dialog. Move the Appeal button into ModerationDetailsDialog (shown only when the label targets your own account or content and was not self-applied), extract AppealForm into its own module, and remove the redundant per-post pill. The account-level pill on the profile header is unchanged. Co-Authored-By: Claude Fable 5 --- src/components/moderation/AppealForm.tsx | 162 ++++++++++++++++++ src/components/moderation/LabelsOnMe.tsx | 40 +---- .../moderation/LabelsOnMeDialog.tsx | 157 +---------------- .../moderation/ModerationDetailsDialog.tsx | 46 +++++ .../components/ThreadItemAnchor.tsx | 2 - .../PostThread/components/ThreadItemPost.tsx | 2 - .../components/ThreadItemTreePost.tsx | 2 - src/screens/Profile/Header/Shell.tsx | 1 - src/view/com/post/Post.tsx | 2 - src/view/com/posts/PostFeedItem.tsx | 2 - 10 files changed, 221 insertions(+), 195 deletions(-) create mode 100644 src/components/moderation/AppealForm.tsx diff --git a/src/components/moderation/AppealForm.tsx b/src/components/moderation/AppealForm.tsx new file mode 100644 index 0000000000..5432672d1e --- /dev/null +++ b/src/components/moderation/AppealForm.tsx @@ -0,0 +1,162 @@ +import {useState} from 'react' +import {View} from 'react-native' +import {type ComAtprotoLabelDefs, ToolsOzoneReportDefs} from '@atproto/api' +import {XRPCError} from '@atproto/api' +import {msg} from '@lingui/core/macro' +import {useLingui} from '@lingui/react' +import {Trans} from '@lingui/react/macro' +import {useMutation} from '@tanstack/react-query' + +import {useLabelSubject} from '#/lib/moderation' +import {useLabelInfo} from '#/lib/moderation/useLabelInfo' +import {makeProfileLink} from '#/lib/routes/links' +import {sanitizeHandle} from '#/lib/strings/handles' +import {logger} from '#/logger' +import {useAgent} from '#/state/session' +import {atoms as a, useBreakpoints} from '#/alf' +import {Admonition} from '#/components/Admonition' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import {InlineLinkText} from '#/components/Link' +import {Loader} from '#/components/Loader' +import * as Toast from '#/components/Toast' +import {Text} from '#/components/Typography' +import {IS_ANDROID} from '#/env' + +export function AppealForm({ + label, + control, + onPressBack, +}: { + label: ComAtprotoLabelDefs.Label + control: Dialog.DialogOuterProps['control'] + onPressBack: () => void +}) { + const {_} = useLingui() + const {labeler, strings} = useLabelInfo(label) + const {gtMobile} = useBreakpoints() + const [details, setDetails] = useState('') + const {subject} = useLabelSubject({label}) + const isAccountReport = 'did' in subject + const agent = useAgent() + const sourceName = labeler + ? sanitizeHandle(labeler.creator.handle, '@') + : label.src + const [error, setError] = useState(null) + + const {mutate, isPending} = useMutation({ + mutationFn: async () => { + const $type = !isAccountReport + ? 'com.atproto.repo.strongRef' + : 'com.atproto.admin.defs#repoRef' + await agent.createModerationReport( + { + reasonType: ToolsOzoneReportDefs.REASONAPPEAL, + subject: { + $type, + ...subject, + }, + reason: details, + }, + { + encoding: 'application/json', + headers: { + 'atproto-proxy': `${label.src}#atproto_labeler`, + }, + }, + ) + }, + onError: err => { + if (err instanceof XRPCError && err.error === 'AlreadyAppealed') { + setError( + _( + msg`You've already appealed this label and it's being reviewed by our moderation team.`, + ), + ) + } else { + setError(_(msg`Failed to submit appeal, please try again.`)) + } + logger.error('Failed to submit label appeal', {message: err}) + }, + onSuccess: () => { + control.close() + Toast.show(_(msg({message: 'Appeal submitted', context: 'toast'}))) + }, + }) + + const onSubmit = () => mutate() + + return ( + <> + + + Appeal "{strings.name}" label + + + + This appeal will be sent to{' '} + control.close()} + style={[a.text_md, a.leading_snug]}> + {sourceName} + + . + + + + {error && ( + + {error} + + )} + + + + + + + + + {IS_ANDROID && } + + ) +} diff --git a/src/components/moderation/LabelsOnMe.tsx b/src/components/moderation/LabelsOnMe.tsx index 126e496887..84edb9b4b8 100644 --- a/src/components/moderation/LabelsOnMe.tsx +++ b/src/components/moderation/LabelsOnMe.tsx @@ -1,5 +1,5 @@ import {type StyleProp, View, type ViewStyle} from 'react-native' -import {type AppBskyFeedDefs, type ComAtprotoLabelDefs} from '@atproto/api' +import {type ComAtprotoLabelDefs} from '@atproto/api' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Plural} from '@lingui/react/macro' @@ -19,12 +19,10 @@ import { } from '#/components/moderation/LabelsOnMeDialog' export function LabelsOnMe({ - type, labels, size, style, }: { - type: 'account' | 'content' labels: ComAtprotoLabelDefs.Label[] | undefined size?: ButtonSize style?: StyleProp @@ -47,7 +45,7 @@ export function LabelsOnMe({ return ( - + ) } - -export function LabelsOnMyPost({ - post, - style, -}: { - post: AppBskyFeedDefs.PostView - style?: StyleProp -}) { - const {currentAccount} = useSession() - if (post.author.did !== currentAccount?.did) { - return null - } - return ( - - ) -} diff --git a/src/components/moderation/LabelsOnMeDialog.tsx b/src/components/moderation/LabelsOnMeDialog.tsx index 83fe2e4019..ac5f256751 100644 --- a/src/components/moderation/LabelsOnMeDialog.tsx +++ b/src/components/moderation/LabelsOnMeDialog.tsx @@ -1,29 +1,22 @@ -import {useCallback, useMemo, useState} from 'react' +import {useMemo, useState} from 'react' import {View} from 'react-native' -import {type ComAtprotoLabelDefs, ToolsOzoneReportDefs} from '@atproto/api' -import {XRPCError} from '@atproto/api' +import {type ComAtprotoLabelDefs} from '@atproto/api' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' -import {useMutation} from '@tanstack/react-query' import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo' -import {useLabelSubject} from '#/lib/moderation' import {useLabelInfo} from '#/lib/moderation/useLabelInfo' import {makeProfileLink} from '#/lib/routes/links' import {sanitizeHandle} from '#/lib/strings/handles' -import {logger} from '#/logger' -import {useAgent, useSession} from '#/state/session' -import {atoms as a, useBreakpoints, useTheme} from '#/alf' -import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import {useSession} from '#/state/session' +import {atoms as a, useTheme} from '#/alf' +import {Button, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import {InlineLinkText} from '#/components/Link' -import * as Toast from '#/components/Toast' +import {AppealForm} from '#/components/moderation/AppealForm' import {Text} from '#/components/Typography' -import {IS_ANDROID} from '#/env' -import {Admonition} from '../Admonition' import {Divider} from '../Divider' -import {Loader} from '../Loader' export {useDialogControl as useLabelsOnMeDialogControl} from '#/components/Dialog' @@ -211,141 +204,3 @@ function Label({ ) } - -function AppealForm({ - label, - control, - onPressBack, -}: { - label: ComAtprotoLabelDefs.Label - control: Dialog.DialogOuterProps['control'] - onPressBack: () => void -}) { - const {_} = useLingui() - const {labeler, strings} = useLabelInfo(label) - const {gtMobile} = useBreakpoints() - const [details, setDetails] = useState('') - const {subject} = useLabelSubject({label}) - const isAccountReport = 'did' in subject - const agent = useAgent() - const sourceName = labeler - ? sanitizeHandle(labeler.creator.handle, '@') - : label.src - const [error, setError] = useState(null) - - const {mutate, isPending} = useMutation({ - mutationFn: async () => { - const $type = !isAccountReport - ? 'com.atproto.repo.strongRef' - : 'com.atproto.admin.defs#repoRef' - await agent.createModerationReport( - { - reasonType: ToolsOzoneReportDefs.REASONAPPEAL, - subject: { - $type, - ...subject, - }, - reason: details, - }, - { - encoding: 'application/json', - headers: { - 'atproto-proxy': `${label.src}#atproto_labeler`, - }, - }, - ) - }, - onError: err => { - if (err instanceof XRPCError && err.error === 'AlreadyAppealed') { - setError( - _( - msg`You've already appealed this label and it's being reviewed by our moderation team.`, - ), - ) - } else { - setError(_(msg`Failed to submit appeal, please try again.`)) - } - logger.error('Failed to submit label appeal', {message: err}) - }, - onSuccess: () => { - control.close() - Toast.show(_(msg({message: 'Appeal submitted', context: 'toast'}))) - }, - }) - - const onSubmit = useCallback(() => mutate(), [mutate]) - - return ( - <> - - - Appeal "{strings.name}" label - - - - This appeal will be sent to{' '} - control.close()} - style={[a.text_md, a.leading_snug]}> - {sourceName} - - . - - - - {error && ( - - {error} - - )} - - - - - - - - - {IS_ANDROID && } - - ) -} diff --git a/src/components/moderation/ModerationDetailsDialog.tsx b/src/components/moderation/ModerationDetailsDialog.tsx index ddc42effa3..d0289496c5 100644 --- a/src/components/moderation/ModerationDetailsDialog.tsx +++ b/src/components/moderation/ModerationDetailsDialog.tsx @@ -1,3 +1,4 @@ +import {useState} from 'react' import {View} from 'react-native' import {type ModerationCause} from '@atproto/api' import {msg} from '@lingui/core/macro' @@ -11,8 +12,10 @@ import {listUriToHref} from '#/lib/strings/url-helpers' import {useSession} from '#/state/session' import {atoms as a, useGutters, useTheme, web} from '#/alf' import {Admonition} from '#/components/Admonition' +import {Button, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import {InlineLinkText} from '#/components/Link' +import {AppealForm} from '#/components/moderation/AppealForm' import {type AppModerationCause} from '#/components/Pills' import {Text} from '#/components/Typography' import {IS_NATIVE} from '#/env' @@ -47,6 +50,18 @@ function ModerationDetailsDialogInner({ const desc = useModerationCauseDescription(modcause) const {currentAccount} = useSession() const timeDiff = useGetTimeAgo({future: true}) + const [isAppealing, setIsAppealing] = useState(false) + + /* + * Appeal eligibility: only for label causes on content belonging to the + * current user, where the label was not self-applied. + */ + const canAppeal = + modcause?.type === 'label' && + !!currentAccount && + modcause.label.src !== currentAccount.did && + (modcause.label.uri === currentAccount.did || + modcause.label.uri.startsWith(`at://${currentAccount.did}/`)) let name let description @@ -137,6 +152,23 @@ function ModerationDetailsDialogInner({ const sourceName = desc.source || desc.sourceDisplayName || _(msg`an unknown labeler`) + if (isAppealing && modcause?.type === 'label') { + return ( + + setIsAppealing(false)} + /> + + + ) + } + return ( )} + {canAppeal && ( + + + + )} )} diff --git a/src/screens/PostThread/components/ThreadItemAnchor.tsx b/src/screens/PostThread/components/ThreadItemAnchor.tsx index dc3c55f125..9439adc8e3 100644 --- a/src/screens/PostThread/components/ThreadItemAnchor.tsx +++ b/src/screens/PostThread/components/ThreadItemAnchor.tsx @@ -42,7 +42,6 @@ import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Tra import {GalleryBleed} from '#/components/images/Gallery' import {Link} from '#/components/Link' import {ContentHider} from '#/components/moderation/ContentHider' -import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe' import {PostAlerts} from '#/components/moderation/PostAlerts' import {type AppModerationCause} from '#/components/Pills' import {Embed, PostEmbedViewContext} from '#/components/Post/Embed' @@ -384,7 +383,6 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({ - - - )} - )} -