diff --git a/src/components/moderation/ReportDialog/action.ts b/src/components/moderation/ReportDialog/action.ts new file mode 100644 index 0000000000..fde2c78988 --- /dev/null +++ b/src/components/moderation/ReportDialog/action.ts @@ -0,0 +1,99 @@ +import { + $Typed, + ChatBskyConvoDefs, + ComAtprotoModerationCreateReport, +} from '@atproto/api' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useMutation} from '@tanstack/react-query' + +import {logger} from '#/logger' +import {useAgent} from '#/state/session' +import {ReportState} from './state' +import {ParsedReportSubject} from './types' + +export function useSubmitReportMutation() { + const {_} = useLingui() + const agent = useAgent() + + return useMutation({ + async mutationFn({ + subject, + state, + }: { + subject: ParsedReportSubject + state: ReportState + }) { + if (!state.selectedOption) { + throw new Error(_(msg`Please select a reason for this report`)) + } + if (!state.selectedLabeler) { + throw new Error(_(msg`Please select a moderation service`)) + } + + let report: + | ComAtprotoModerationCreateReport.InputSchema + | (Omit & { + subject: $Typed + }) + + switch (subject.type) { + case 'account': { + report = { + reasonType: state.selectedOption.reason, + reason: state.details, + subject: { + $type: 'com.atproto.admin.defs#repoRef', + did: subject.did, + }, + } + break + } + case 'post': + case 'list': + case 'feed': + case 'starterPack': { + report = { + reasonType: state.selectedOption.reason, + reason: state.details, + subject: { + $type: 'com.atproto.repo.strongRef', + uri: subject.uri, + cid: subject.cid, + }, + } + break + } + case 'chatMessage': { + report = { + reasonType: state.selectedOption.reason, + reason: state.details, + subject: { + $type: 'chat.bsky.convo.defs#messageRef', + messageId: subject.message.id, + convoId: subject.convoId, + did: subject.message.sender.did, + }, + } + break + } + } + + if (__DEV__) { + logger.info('Submitting report', { + labeler: { + handle: state.selectedLabeler.creator.handle, + }, + report, + }) + } else { + await agent.createModerationReport(report, { + encoding: 'application/json', + headers: { + 'atproto-proxy': `${state.selectedLabeler.creator.did}#atproto_labeler`, + }, + }) + } + }, + }) +} diff --git a/src/components/moderation/ReportDialog/const.ts b/src/components/moderation/ReportDialog/const.ts new file mode 100644 index 0000000000..2b34633606 --- /dev/null +++ b/src/components/moderation/ReportDialog/const.ts @@ -0,0 +1,98 @@ +import {ComAtprotoModerationDefs} from '@atproto/api' + +import {ParsedReportSubject} from './types' + +export const ALL_REPORT_REASON_TYPES = [ + 'com.atproto.moderation.defs#reasonSpam', + 'com.atproto.moderation.defs#reasonViolation', + 'com.atproto.moderation.defs#reasonMisleading', + 'com.atproto.moderation.defs#reasonSexual', + 'com.atproto.moderation.defs#reasonRude', + 'com.atproto.moderation.defs#reasonHarassment', + 'com.atproto.moderation.defs#reasonAnimalAbuse', + 'com.atproto.moderation.defs#reasonGraphic', + 'com.atproto.moderation.defs#reasonScam', + 'com.atproto.moderation.defs#reasonProhibitedCommerce', + 'com.atproto.moderation.defs#reasonInauthentic', + 'com.atproto.moderation.defs#reasonImpersonation', + 'com.atproto.moderation.defs#reasonBanEvasion', + 'com.atproto.moderation.defs#reasonChildSafety', + 'com.atproto.moderation.defs#reasonSelfHarm', + 'com.atproto.moderation.defs#reasonThreat', + 'com.atproto.moderation.defs#reasonIntolerance', + 'com.atproto.moderation.defs#reasonPrivateInfo', + 'com.atproto.moderation.defs#reasonSexualAbuse', + 'com.atproto.moderation.defs#reasonSiteSecurity', + 'com.atproto.moderation.defs#reasonTerrorViolentExtremist', + 'com.atproto.moderation.defs#reasonSpoiler', + 'com.atproto.moderation.defs#reasonGrowthHack', + 'com.atproto.moderation.defs#reasonOther', +] + +const REASONHARRASSMENT = 'com.atproto.moderation.defs#reasonHarassment' +const REASONINAUTHENTIC = 'com.atproto.moderation.defs#reasonInauthentic' +const REASONIMPERSONATION = 'com.atproto.moderation.defs#reasonImpersonation' +const REASONBANEVASION = 'com.atproto.moderation.defs#reasonBanEvasion' +const REASONTERRORVIOLENTEXTREMIST = + 'com.atproto.moderation.defs#reasonTerrorViolentExtremist' +const REASONANIMALABUSE = 'com.atproto.moderation.defs#reasonAnimalAbuse' +const REASONGRAPHIC = 'com.atproto.moderation.defs#reasonGraphic' +const REASONSCAM = 'com.atproto.moderation.defs#reasonScam' +const REASONPROHIBITEDCOMMERCE = + 'com.atproto.moderation.defs#reasonProhibitedCommerce' +const REASONCHILDSAFETY = 'com.atproto.moderation.defs#reasonChildSafety' +const REASONSELFHARM = 'com.atproto.moderation.defs#reasonSelfHarm' +const REASONTHREAT = 'com.atproto.moderation.defs#reasonThreat' +const REASONINTOLERANCE = 'com.atproto.moderation.defs#reasonIntolerance' +const REASONPRIVATEINFO = 'com.atproto.moderation.defs#reasonPrivateInfo' +const REASONSEXUALABUSE = 'com.atproto.moderation.defs#reasonSexualAbuse' +const REASONSITESECURITY = 'com.atproto.moderation.defs#reasonSiteSecurity' + +const RECORD_REPORT_OPTIONS = [ + ComAtprotoModerationDefs.REASONSPAM, + REASONHARRASSMENT, + ComAtprotoModerationDefs.REASONMISLEADING, + ComAtprotoModerationDefs.REASONSEXUAL, + REASONANIMALABUSE, + REASONGRAPHIC, + REASONSCAM, + REASONPROHIBITEDCOMMERCE, + REASONCHILDSAFETY, + REASONSELFHARM, + REASONTHREAT, + REASONINTOLERANCE, + REASONPRIVATEINFO, + REASONSEXUALABUSE, + REASONSITESECURITY, + REASONTERRORVIOLENTEXTREMIST, + + ComAtprotoModerationDefs.REASONOTHER, +] + +export const REPORT_OPTIONS: Record = { + account: [ + REASONHARRASSMENT, + REASONINAUTHENTIC, + REASONIMPERSONATION, + REASONBANEVASION, + REASONTERRORVIOLENTEXTREMIST, + ComAtprotoModerationDefs.REASONSPAM, + ComAtprotoModerationDefs.REASONMISLEADING, + ComAtprotoModerationDefs.REASONOTHER, + ], + post: RECORD_REPORT_OPTIONS, + list: RECORD_REPORT_OPTIONS, + feed: RECORD_REPORT_OPTIONS, + starterPack: RECORD_REPORT_OPTIONS, + chatMessage: [ + ComAtprotoModerationDefs.REASONSPAM, + REASONHARRASSMENT, + ComAtprotoModerationDefs.REASONSEXUAL, + REASONSCAM, + REASONCHILDSAFETY, + REASONSELFHARM, + REASONTHREAT, + REASONPRIVATEINFO, + ComAtprotoModerationDefs.REASONOTHER, + ], +} diff --git a/src/components/moderation/ReportDialog/copy.ts b/src/components/moderation/ReportDialog/copy.ts new file mode 100644 index 0000000000..87e199f989 --- /dev/null +++ b/src/components/moderation/ReportDialog/copy.ts @@ -0,0 +1,49 @@ +import {useMemo} from 'react' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {ParsedReportSubject} from './types' + +export function useCopyForSubject(subject: ParsedReportSubject) { + const {_} = useLingui() + return useMemo(() => { + switch (subject.type) { + case 'account': { + return { + title: _(msg`Report this user`), + subtitle: _(msg`Why should this user be reviewed?`), + } + } + case 'post': { + return { + title: _(msg`Report this post`), + subtitle: _(msg`Why should this post be reviewed?`), + } + } + case 'list': { + return { + title: _(msg`Report this list`), + subtitle: _(msg`Why should this list be reviewed?`), + } + } + case 'feed': { + return { + title: _(msg`Report this feed`), + subtitle: _(msg`Why should this feed be reviewed?`), + } + } + case 'starterPack': { + return { + title: _(msg`Report this starter pack`), + subtitle: _(msg`Why should this starter pack be reviewed?`), + } + } + case 'chatMessage': { + return { + title: _(msg`Report this message`), + subtitle: _(msg`Why should this message be reviewed?`), + } + } + } + }, [_, subject]) +} diff --git a/src/components/moderation/ReportDialog/index.tsx b/src/components/moderation/ReportDialog/index.tsx new file mode 100644 index 0000000000..1f113561bf --- /dev/null +++ b/src/components/moderation/ReportDialog/index.tsx @@ -0,0 +1,518 @@ +import React from 'react' +import {Pressable, View} from 'react-native' +import {ScrollView} from 'react-native-gesture-handler' +import {AppBskyLabelerDefs} from '@atproto/api' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {wait} from '#/lib/async/wait' +import {getLabelingServiceTitle} from '#/lib/moderation' +import {sanitizeHandle} from '#/lib/strings/handles' +import {logger} from '#/logger' +import {isNative} from '#/platform/detection' +import {useMyLabelersQuery} from '#/state/queries/preferences' +import {CharProgress} from '#/view/com/composer/char-progress/CharProgress' +import {UserAvatar} from '#/view/com/util/UserAvatar' +import {atoms as a, useGutters,useTheme} from '#/alf' +import {Admonition} from '#/components/Admonition' +import {Button, ButtonIcon,ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import {useDelayedLoading} from '#/components/hooks/useDelayedLoading' +import { + Check_Stroke2_Corner0_Rounded as CheckThin, + CheckThick_Stroke2_Corner0_Rounded as Check, +} from '#/components/icons/Check' +import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlane} from '#/components/icons/PaperPlane' +import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' +import {createStaticClick,InlineLinkText} from '#/components/Link' +import {Loader} from '#/components/Loader' +import {Text} from '#/components/Typography' +import {useSubmitReportMutation} from './action' +import {useCopyForSubject} from './copy' +import {initialState, reducer} from './state' +import {ReportDialogProps, ReportSubject} from './types' +import {parseReportSubject} from './utils/parseReportSubject' +import {ReportOption,useReportOptions} from './utils/useReportOptions' + +export {useDialogControl as useReportDialogControl} from '#/components/Dialog' + +export function ReportDialog( + props: Omit & { + subject: ReportSubject + }, +) { + const subject = React.useMemo( + () => parseReportSubject(props.subject), + [props.subject], + ) + return ( + + + {subject ? : } + + ) +} + +// TODO +function Invalid() { + const {_} = useLingui() + return ( + + + Invalid report subject + + + This is a developer error. + + + + ) +} + +function Inner(props: ReportDialogProps) { + const {_} = useLingui() + const ref = React.useRef(null) + const { + isLoading: isLabelerLoading, + data: allLabelers, + error: labelersLoadError, + } = useMyLabelersQuery({excludeNonConfigurableLabelers: true}) + const isLoading = useDelayedLoading(500, isLabelerLoading) + + const copy = useCopyForSubject(props.subject) + const allOptions = useReportOptions() + const options = allOptions[props.subject.type] + + const [state, dispatch] = React.useReducer(reducer, initialState) + + /** + * Labelers that support this `subject` and its NSID collection + */ + const supportedLabelers = React.useMemo(() => { + if (!allLabelers) return [] + return allLabelers + .filter(l => { + // @ts-ignore + const subjectTypes: string[] | undefined = l.subjectTypes + if (subjectTypes === undefined) return true + if (props.subject.type === 'account') { + return subjectTypes.includes('account') + } else if (props.subject.type === 'chatMessage') { + return subjectTypes.includes('chat') + } else { + return subjectTypes.includes('record') + } + }) + .filter(l => { + // @ts-ignore + const collections: string[] | undefined = l.subjectCollections + if (collections === undefined) return true + // all chat collections accepted, since only Bluesky handles chats + if (props.subject.type === 'chatMessage') return true + return collections.includes(props.subject.nsid) + }) + .filter(l => { + if (!state.selectedOption) return true + // @ts-ignore + const reasonTypes: string[] | undefined = l.reasonTypes + if (reasonTypes === undefined) return true + return reasonTypes.includes(state.selectedOption.reason) + }) + }, [props, allLabelers, state.selectedOption]) + + const {mutateAsync: submitReport} = useSubmitReportMutation() + const [isPending, setPending] = React.useState(false) + const [isSuccess, setSuccess] = React.useState(false) + + const onSubmit = React.useCallback(async () => { + dispatch({type: 'clearError'}) + + try { + setPending(true) + await wait( + 1e3, + submitReport({ + subject: props.subject, + state, + }), + ) + setSuccess(true) + setTimeout(() => { + props.control.close() + }, 1e3) + } catch (e: any) { + logger.error(e, { + source: 'ReportDialog', + }) + dispatch({ + type: 'setError', + error: _(msg`Something went wrong. Please try again.`), + }) + } finally { + setPending(false) + } + }, [_, submitReport, state, dispatch, props, setPending, setSuccess]) + + return ( + + + + + {isLoading ? ( + + + {/* Here to capture focus for a hot sec to prevent flash */} + + + ) : labelersLoadError || !allLabelers ? ( + + + Something went wrong, please try again. + + + ) : ( + <> + {state.selectedOption ? ( + + + + + + + ) : ( + + {options.map(o => ( + { + dispatch({type: 'selectOption', option: o}) + }} + /> + ))} + + )} + + )} + + + + + {state.activeStepIndex1 >= 2 && ( + <> + {state.selectedLabeler ? ( + + + + + + + ) : ( + <> + {supportedLabelers.length ? ( + + {supportedLabelers.map(l => ( + { + // @ts-expect-error TODO + dispatch({type: 'selectLabeler', labeler: l}) + }} + /> + ))} + + ) : ( + TODO + )} + + )} + + )} + + + + + {state.activeStepIndex1 === 3 && ( + <> + + + + Your report will be sent to{' '} + + {state.selectedLabeler?.creator.displayName} + + .{' '} + {!state.detailsOpen ? ( + { + dispatch({type: 'showDetails'}) + })}> + Add more details (optional) + + ) : null} + + + + {state.detailsOpen && ( + + { + dispatch({type: 'setDetails', details}) + }} + label={_(msg`Additional details`)} + style={{paddingRight: 60}} + numberOfLines={4} + /> + + + + + )} + + + + {state.error && ( + {state.error} + )} + + )} + + + + ) +} + +function StepOuter({children}: {children: React.ReactNode}) { + return {children} +} + +function StepTitle({ + index, + title, + activeIndex1, +}: { + index: number + title: string + activeIndex1: number +}) { + const t = useTheme() + const active = activeIndex1 === index + const completed = activeIndex1 > index + return ( + + + {completed ? ( + + ) : ( + + {index} + + )} + + + + {title} + + + ) +} + +function OptionCard({ + option, + onSelect, +}: { + option: ReportOption + onSelect?: (option: ReportOption) => void +}) { + const t = useTheme() + const {_} = useLingui() + const gutters = useGutters(['compact']) + const onPress = React.useCallback(() => { + onSelect?.(option) + }, [onSelect, option]) + return ( + + ) +} + +function LabelerCard({ + labeler, + onSelect, +}: { + labeler: AppBskyLabelerDefs.LabelerViewDetailed + onSelect?: (option: AppBskyLabelerDefs.LabelerViewDetailed) => void +}) { + const t = useTheme() + const {_} = useLingui() + const onPress = React.useCallback(() => { + onSelect?.(labeler) + }, [onSelect, labeler]) + const title = getLabelingServiceTitle({ + displayName: labeler.creator.displayName, + handle: labeler.creator.handle, + }) + return ( + + ) +} diff --git a/src/components/moderation/ReportDialog/state.ts b/src/components/moderation/ReportDialog/state.ts new file mode 100644 index 0000000000..18cd3a4af2 --- /dev/null +++ b/src/components/moderation/ReportDialog/state.ts @@ -0,0 +1,102 @@ +import {AppBskyLabelerDefs} from '@atproto/api' + +import {ReportOption} from './utils/useReportOptions' + +export type ReportState = { + selectedOption?: ReportOption + selectedLabeler?: AppBskyLabelerDefs.LabelerViewDetailed + details?: string + detailsOpen: boolean + activeStepIndex1: number + error?: string +} + +export type ReportAction = + | { + type: 'selectOption' + option: ReportOption + } + | { + type: 'clearOption' + } + | { + type: 'selectLabeler' + labeler: AppBskyLabelerDefs.LabelerViewDetailed + } + | { + type: 'clearLabeler' + } + | { + type: 'setDetails' + details: string + } + | { + type: 'setError' + error: string + } + | { + type: 'clearError' + } + | { + type: 'showDetails' + } + +export const initialState: ReportState = { + selectedOption: undefined, + selectedLabeler: undefined, + details: undefined, + detailsOpen: false, + activeStepIndex1: 1, +} + +export function reducer(state: ReportState, action: ReportAction): ReportState { + switch (action.type) { + case 'selectOption': + return { + ...state, + selectedOption: action.option, + activeStepIndex1: 2, + } + case 'clearOption': + return { + ...state, + selectedOption: undefined, + selectedLabeler: undefined, + activeStepIndex1: 1, + detailsOpen: !!state.details, + } + case 'selectLabeler': + return { + ...state, + selectedLabeler: action.labeler, + activeStepIndex1: 3, + } + case 'clearLabeler': + return { + ...state, + selectedLabeler: undefined, + activeStepIndex1: 2, + detailsOpen: !!state.details, + } + case 'setDetails': + return { + ...state, + details: action.details, + } + case 'setError': + return { + ...state, + error: action.error, + } + case 'clearError': + return { + ...state, + error: undefined, + } + case 'showDetails': + return { + ...state, + detailsOpen: true, + } + } +} diff --git a/src/components/moderation/ReportDialog/types.ts b/src/components/moderation/ReportDialog/types.ts new file mode 100644 index 0000000000..100eada738 --- /dev/null +++ b/src/components/moderation/ReportDialog/types.ts @@ -0,0 +1,69 @@ +import { + $Typed, + AppBskyActorDefs, + AppBskyFeedDefs, + AppBskyGraphDefs, + ChatBskyConvoDefs, +} from '@atproto/api' + +import * as Dialog from '#/components/Dialog' + +export type ReportSubject = + | $Typed + | $Typed + | $Typed + | $Typed + | $Typed + | $Typed + | $Typed + | {convoId: string; message: ChatBskyConvoDefs.MessageView} + +export type ParsedReportSubject = + | { + type: 'post' + uri: string + cid: string + nsid: string + attributes: { + reply: boolean + image: boolean + video: boolean + link: boolean + quote: boolean + } + } + | { + type: 'list' + uri: string + cid: string + nsid: string + } + | { + type: 'feed' + uri: string + cid: string + nsid: string + } + | { + type: 'starterPack' + uri: string + cid: string + nsid: string + } + | { + type: 'account' + did: string + nsid: string + } + | { + type: 'chatMessage' + convoId: string + message: ChatBskyConvoDefs.MessageView + } + +export type ReportDialogProps = { + control: Dialog.DialogOuterProps['control'] + subject: ParsedReportSubject + // TODO handle convo report navigation + onFinishReport?: () => void +} diff --git a/src/components/moderation/ReportDialog/utils/parseReportSubject.ts b/src/components/moderation/ReportDialog/utils/parseReportSubject.ts new file mode 100644 index 0000000000..b79e496958 --- /dev/null +++ b/src/components/moderation/ReportDialog/utils/parseReportSubject.ts @@ -0,0 +1,91 @@ +import { + AppBskyActorDefs, + AppBskyFeedDefs, + AppBskyFeedPost, + AppBskyGraphDefs, +} from '@atproto/api' + +import { + ParsedReportSubject, + ReportSubject, +} from '#/components/moderation/ReportDialog/types' +import * as bsky from '#/types/bsky' + +export function parseReportSubject( + subject: ReportSubject, +): ParsedReportSubject | undefined { + if (!subject) return + + if ('convoId' in subject) { + return { + type: 'chatMessage', + ...subject, + } + } + + if ( + AppBskyActorDefs.isProfileViewBasic(subject) || + AppBskyActorDefs.isProfileView(subject) || + AppBskyActorDefs.isProfileViewDetailed(subject) + ) { + return { + type: 'account', + did: subject.did, + nsid: 'app.bsky.actor.profile', + } + } else if (AppBskyGraphDefs.isListView(subject)) { + return { + type: 'list', + uri: subject.uri, + cid: subject.cid, + nsid: 'app.bsky.graph.list', + } + } else if (AppBskyFeedDefs.isGeneratorView(subject)) { + return { + type: 'feed', + uri: subject.uri, + cid: subject.cid, + nsid: 'app.bsky.feed.generator', + } + } else if (AppBskyGraphDefs.isStarterPackView(subject)) { + return { + type: 'starterPack', + uri: subject.uri, + cid: subject.cid, + nsid: 'app.bsky.graph.starterPack', + } + } else if (AppBskyFeedDefs.isPostView(subject)) { + const record = subject.record + const embed = bsky.post.parseEmbed(subject.embed) + if ( + bsky.dangerousIsType( + record, + AppBskyFeedPost.isRecord, + ) + ) { + return { + type: 'post', + uri: subject.uri, + cid: subject.cid, + nsid: 'app.bsky.feed.post', + attributes: { + reply: !!record.reply, + image: + embed.type === 'images' || + (embed.type === 'post_with_media' && embed.media.type === 'images'), + video: + embed.type === 'video' || + (embed.type === 'post_with_media' && embed.media.type === 'video'), + link: + embed.type === 'link' || + (embed.type === 'post_with_media' && embed.media.type === 'link'), + quote: + embed.type === 'post' || + (embed.type === 'post_with_media' && + (embed.view.type === 'post' || + embed.view.type === 'post_with_media')), + }, + } + } + } +} diff --git a/src/components/moderation/ReportDialog/utils/useReportOptions.ts b/src/components/moderation/ReportDialog/utils/useReportOptions.ts new file mode 100644 index 0000000000..38888d51a4 --- /dev/null +++ b/src/components/moderation/ReportDialog/utils/useReportOptions.ts @@ -0,0 +1,121 @@ +import {useMemo} from 'react' +import {ComAtprotoModerationDefs} from '@atproto/api' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +export interface ReportOption { + reason: string + title: string + description: string +} + +interface ReportOptions { + account: ReportOption[] + post: ReportOption[] + list: ReportOption[] + starterPack: ReportOption[] + feed: ReportOption[] + chatMessage: ReportOption[] +} + +export function useReportOptions(): ReportOptions { + const {_} = useLingui() + + return useMemo(() => { + const other = { + reason: ComAtprotoModerationDefs.REASONOTHER, + title: _(msg`Other`), + description: _(msg`An issue not included in these options`), + } + const common = [ + { + reason: ComAtprotoModerationDefs.REASONRUDE, + title: _(msg`Anti-Social Behavior`), + description: _(msg`Harassment, trolling, or intolerance`), + }, + { + reason: ComAtprotoModerationDefs.REASONVIOLATION, + title: _(msg`Illegal and Urgent`), + description: _(msg`Glaring violations of law or terms of service`), + }, + other, + ] + return { + account: [ + { + reason: ComAtprotoModerationDefs.REASONMISLEADING, + title: _(msg`Misleading Account`), + description: _( + msg`Impersonation or false claims about identity or affiliation`, + ), + }, + { + reason: ComAtprotoModerationDefs.REASONSPAM, + title: _(msg`Frequently Posts Unwanted Content`), + description: _(msg`Spam; excessive mentions or replies`), + }, + { + reason: ComAtprotoModerationDefs.REASONVIOLATION, + title: _(msg`Name or Description Violates Community Standards`), + description: _(msg`Terms used violate community standards`), + }, + other, + ], + post: [ + { + reason: ComAtprotoModerationDefs.REASONMISLEADING, + title: _(msg`Misleading Post`), + description: _(msg`Impersonation, misinformation, or false claims`), + }, + { + reason: ComAtprotoModerationDefs.REASONSPAM, + title: _(msg`Spam`), + description: _(msg`Excessive mentions or replies`), + }, + { + reason: ComAtprotoModerationDefs.REASONSEXUAL, + title: _(msg`Unwanted Sexual Content`), + description: _(msg`Nudity or adult content not labeled as such`), + }, + ...common, + ], + chatMessage: [ + { + reason: ComAtprotoModerationDefs.REASONSPAM, + title: _(msg`Spam`), + description: _(msg`Excessive or unwanted messages`), + }, + { + reason: ComAtprotoModerationDefs.REASONSEXUAL, + title: _(msg`Unwanted Sexual Content`), + description: _(msg`Inappropriate messages or explicit links`), + }, + ...common, + ], + list: [ + { + reason: ComAtprotoModerationDefs.REASONVIOLATION, + title: _(msg`Name or Description Violates Community Standards`), + description: _(msg`Terms used violate community standards`), + }, + ...common, + ], + starterPack: [ + { + reason: ComAtprotoModerationDefs.REASONVIOLATION, + title: _(msg`Name or Description Violates Community Standards`), + description: _(msg`Terms used violate community standards`), + }, + ...common, + ], + feed: [ + { + reason: ComAtprotoModerationDefs.REASONVIOLATION, + title: _(msg`Name or Description Violates Community Standards`), + description: _(msg`Terms used violate community standards`), + }, + ...common, + ], + } + }, [_]) +}