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({ - - - )} - )} -