diff --git a/src/components/moderation/ReportDialog/index.tsx b/src/components/moderation/ReportDialog/index.tsx index ebfbe0dcd3..14b58d9fd2 100644 --- a/src/components/moderation/ReportDialog/index.tsx +++ b/src/components/moderation/ReportDialog/index.tsx @@ -35,7 +35,11 @@ import {useCopyForSubject} from './copy' import {initialState, reducer} from './state' import {type ReportDialogProps, type ReportSubject} from './types' import {parseReportSubject} from './utils/parseReportSubject' -import {type ReportOption, useReportOptions} from './utils/useReportOptions' +import { + type ReportOption, + TopLevelReportOption, + useReportOptions, +} from './utils/useReportOptions' export {useDialogControl as useReportDialogControl} from '#/components/Dialog' @@ -95,7 +99,7 @@ function Inner(props: ReportDialogProps) { } = useMyLabelersQuery({excludeNonConfigurableLabelers: true}) const isLoading = useDelayedLoading(500, isLabelerLoading) const copy = useCopyForSubject(props.subject) - const reportOptions = useReportOptions() + const {reasons, sortedReasons, getTopLevelReason} = useReportOptions() const [state, dispatch] = React.useReducer(reducer, initialState) /** @@ -237,32 +241,32 @@ function Inner(props: ReportDialogProps) { ) : ( <> - {state.selectedOption ? ( + {state.selectedTopLevelOption ? ( - + ) : ( - {reportOptions[props.subject.type].map(o => ( - ( + { - dispatch({type: 'selectOption', option: o}) + dispatch({type: 'selectTopLevelOption', option: o}) }} /> ))} @@ -307,6 +311,82 @@ function Inner(props: ReportDialogProps) { )} + + + {state.selectedOption ? ( + + + + + + + ) : state.selectedTopLevelOption ? ( + + {getTopLevelReason(state.selectedTopLevelOption.key).options.map( + o => ( + { + dispatch({type: 'selectOption', option: o}) + }} + /> + ), + )} + + {['post', 'account'].includes(props.subject.type) && ( + + {({hovered, pressed}) => ( + + + + Need to report a copyright violation, legal request, + or regulatory compliance issue? + + + + + )} + + )} + + ) : null} + + void + option: TopLevelReportOption + onSelect?: (option: TopLevelReportOption) => void }) { const t = useTheme() const {_} = useLingui() @@ -584,7 +664,7 @@ function OptionCard({ }, [onSelect, option]) return ( + ) +} + function OptionCardSkeleton() { const t = useTheme() return ( diff --git a/src/components/moderation/ReportDialog/state.ts b/src/components/moderation/ReportDialog/state.ts index 18ee77df8d..0e9c57f854 100644 --- a/src/components/moderation/ReportDialog/state.ts +++ b/src/components/moderation/ReportDialog/state.ts @@ -1,8 +1,9 @@ import {type AppBskyLabelerDefs, ComAtprotoModerationDefs} from '@atproto/api' -import {type ReportOption} from './utils/useReportOptions' +import {TopLevelReportOption, type ReportOption} from './utils/useReportOptions' export type ReportState = { + selectedTopLevelOption?: TopLevelReportOption selectedOption?: ReportOption selectedLabeler?: AppBskyLabelerDefs.LabelerViewDetailed details?: string @@ -12,6 +13,13 @@ export type ReportState = { } export type ReportAction = + | { + type: 'selectTopLevelOption' + option: TopLevelReportOption + } + | { + type: 'clearTopLevelOption' + } | { type: 'selectOption' option: ReportOption @@ -42,6 +50,7 @@ export type ReportAction = } export const initialState: ReportState = { + selectedTopLevelOption: undefined, selectedOption: undefined, selectedLabeler: undefined, details: undefined, @@ -51,11 +60,29 @@ export const initialState: ReportState = { export function reducer(state: ReportState, action: ReportAction): ReportState { switch (action.type) { + case 'selectTopLevelOption': + return { + ...state, + selectedTopLevelOption: action.option, + activeStepIndex1: 2, + detailsOpen: !!state.details, + } + case 'clearTopLevelOption': + return { + ...state, + selectedTopLevelOption: undefined, + selectedOption: undefined, + selectedLabeler: undefined, + activeStepIndex1: 1, + detailsOpen: + !!state.details || + state.selectedOption?.reason === ComAtprotoModerationDefs.REASONOTHER, + } case 'selectOption': return { ...state, selectedOption: action.option, - activeStepIndex1: 2, + activeStepIndex1: 3, detailsOpen: !!state.details || action.option.reason === ComAtprotoModerationDefs.REASONOTHER, @@ -65,7 +92,7 @@ export function reducer(state: ReportState, action: ReportAction): ReportState { ...state, selectedOption: undefined, selectedLabeler: undefined, - activeStepIndex1: 1, + activeStepIndex1: 2, detailsOpen: !!state.details || state.selectedOption?.reason === ComAtprotoModerationDefs.REASONOTHER, @@ -74,13 +101,13 @@ export function reducer(state: ReportState, action: ReportAction): ReportState { return { ...state, selectedLabeler: action.labeler, - activeStepIndex1: 3, + activeStepIndex1: 4, } case 'clearLabeler': return { ...state, selectedLabeler: undefined, - activeStepIndex1: 2, + activeStepIndex1: 3, detailsOpen: !!state.details || state.selectedOption?.reason === ComAtprotoModerationDefs.REASONOTHER, diff --git a/src/components/moderation/ReportDialog/utils/useReportOptions.ts b/src/components/moderation/ReportDialog/utils/useReportOptions.ts index 38888d51a4..0e7c733861 100644 --- a/src/components/moderation/ReportDialog/utils/useReportOptions.ts +++ b/src/components/moderation/ReportDialog/utils/useReportOptions.ts @@ -1,121 +1,307 @@ -import {useMemo} from 'react' -import {ComAtprotoModerationDefs} from '@atproto/api' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' -export interface ReportOption { - reason: string +export type TopLevelReason = + | 'childSafety' + | 'violencePhysicalHarm' + | 'sexualAdultContent' + | 'harassmentHate' + | 'misleading' + | 'ruleBreaking' + | 'civicIntegrity' + | 'other' + +export interface TopLevelReportOption { + key: TopLevelReason title: string description: string + options: ReportOption[] + sort: number + isOther?: boolean } -interface ReportOptions { - account: ReportOption[] - post: ReportOption[] - list: ReportOption[] - starterPack: ReportOption[] - feed: ReportOption[] - chatMessage: ReportOption[] +export interface TopLevelReportOptions { + childSafety: TopLevelReportOption + violencePhysicalHarm: TopLevelReportOption + sexualAdultContent: TopLevelReportOption + harassmentHate: TopLevelReportOption + misleading: TopLevelReportOption + ruleBreaking: TopLevelReportOption + civicIntegrity: TopLevelReportOption + other: TopLevelReportOption } -export function useReportOptions(): ReportOptions { +export interface ReportOption { + title: string + reason: string +} + +export function useReportOptions() { const {_} = useLingui() - return useMemo(() => { - const other = { - reason: ComAtprotoModerationDefs.REASONOTHER, + const reasons = { + childSafety: { + key: 'childSafety', + title: _(msg`Child Safety`), + description: _( + msg`Content harming or endangering minors' safety and wellbeing`, + ), + sort: 1, + options: [ + { + title: _(msg`Child Sexual Abuse Material (CSAM)`), + reason: '#reasonChildSafetyCSAM', + }, + { + title: _(msg`Grooming or Predatory Behavior`), + reason: '#reasonChildSafetyGroom', + }, + { + title: _(msg`Minor Privacy Violation`), + reason: '#reasonChildSafetyMinorPrivacy', + }, + { + title: _(msg`Child Endangerment`), + reason: '#reasonChildSafetyEndangerment', + }, + { + title: _(msg`Minor Harassment or Bullying`), + reason: '#reasonChildSafetyHarassment', + }, + { + title: _(msg`Promotion of Child Exploitation`), + reason: '#reasonChildSafetyPromotion', + }, + { + title: _(msg`Other Child Safety`), + reason: '#reasonChildSafetyOther', + }, + ], + }, + violencePhysicalHarm: { + key: 'violencePhysicalHarm', + title: _(msg`Violence of Physical Harm`), + sort: 2, + description: _(msg`Threats, calls for violence, or graphic content`), + options: [ + { + title: _(msg`Animal Welfare`), + reason: '#reasonViolenceAnimalWelfare', + }, + { + title: _(msg`Threats or Inticement`), + reason: '#reasonViolenceThreats', + }, + { + title: _(msg`Graphic Violent Content`), + reason: '#reasonViolenceGraphicContent', + }, + { + title: _(msg`Self Harm`), + reason: '#reasonViolenceSelfHarm', + }, + { + title: _(msg`Glorification of Violence`), + reason: '#reasonViolenceGlorification', + }, + { + title: _(msg`Extermist Content`), + reason: '#reasonViolenceExtermistContent', + }, + { + title: _(msg`Human Trafficking`), + reason: '#reasonViolenceTrafficking', + }, + { + title: _(msg`Other Violent Content`), + reason: '#reasonViolenceOther', + }, + ], + }, + sexualAdultContent: { + key: 'sexualAdultContent', + title: _(msg`Sexaul and Adult Content`), + description: _(msg`Adult, child or animal sexual abuse`), + sort: 3, + options: [ + { + title: _(msg`Adult Sexual Abuse Content`), + reason: '#reasonSexualAbuseContent', + }, + { + title: _(msg`Non-Consensual Intimate Imagery`), + reason: '#reasonSexualNCII', + }, + { + title: _(msg`SexualSextortion`), + reason: '#reasonSexualSextortion', + }, + { + title: _(msg`Deepfake Adult Content`), + reason: '#reasonSexualDeepfake', + }, + { + title: _(msg`Animal Sexual Abuse`), + reason: '#reasonSexualAnimal', + }, + { + title: _(msg`Unlabelled Adult Content`), + reason: '#reasonSexualUnlabelled', + }, + { + title: _(msg`Other Sexual Violence Content`), + reason: '#reasonSexualOther', + }, + ], + }, + harassmentHate: { + key: 'harassmentHate', + title: _(msg`Harassment or Hate`), + description: _( + msg`Targeted attacks, hate speech, or organized harassment`, + ), + sort: 4, + options: [ + { + title: _(msg`Trolling`), + reason: '#reasonHarassmentTroll', + }, + { + title: _(msg`Targeted Harassment`), + reason: '#reasonHarassmentTargeted', + }, + { + title: _(msg`Hate Speech`), + reason: '#reasonHarassmentHateSpeech', + }, + { + title: _(msg`Doxxing`), + reason: '#reasonHarassmentDoxxing', + }, + { + title: _(msg`Other Harassing or Hateful Content`), + reason: '#reasonHarassmentOther', + }, + ], + }, + misleading: { + key: 'misleading', + title: _(msg`Misleading`), + description: _(msg`Spam, scams, false info or impersonation`), + sort: 5, + options: [ + { + title: _(msg`Fake Account or Bot`), + reason: '#reasonMisleadingBot', + }, + { + title: _(msg`Impersonation`), + reason: '#reasonMisleadingImpersonation', + }, + { + title: _(msg`Spam`), + reason: '#reasonMisleadingSpam', + }, + { + title: _(msg`Scam`), + reason: '#reasonMisleadingScam', + }, + { + title: _(msg`Unlabelled GenAI or Synthetic Content`), + reason: '#reasonMisleadingSyntheticContent', + }, + { + title: _(msg`Harmful False Claims`), + reason: '#reasonMisleadingMisinformation', + }, + { + title: _(msg`Other Misleading Content`), + reason: '#reasonMisleadingOther', + }, + ], + }, + ruleBreaking: { + key: 'ruleBreaking', + title: _(msg`Breaking Network Rules`), + description: _(msg`Hacking, stolen content or prohibited sales`), + sort: 6, + options: [ + { + title: _(msg`Hacking or System Attacks`), + reason: '#reasonRuleSiteSecurity', + }, + { + title: _(msg`Stolen Content`), + reason: '#reasonRuleStolenContent', + }, + { + title: _(msg`Promoting or Selling Prohibited Items of Services`), + reason: '#reasonRuleProhibitedSales', + }, + { + title: _(msg`Banned User Returning`), + reason: '#reasonRuleBanEvasion', + }, + { + title: _(msg`Other`), + reason: '#reasonRuleOther', + }, + ], + }, + civicIntegrity: { + key: 'civicIntegrity', + title: _(msg`Civic Integrity`), + description: _( + msg`Electoral interference or political process violations`, + ), + sort: 7, + options: [ + { + title: _(msg`Electoral Process Violations`), + reason: '#reasonCivicElectoralProcess', + }, + { + title: _(msg`Disclosure and Transparency Violations`), + reason: '#reasonCivicDisclosure', + }, + { + title: _(msg`Voter Intimidation or Interference`), + reason: '#reasonCivicInterference', + }, + { + title: _(msg`Election Misinformation`), + reason: '#reasonCivicMisinformation', + }, + { + title: _(msg`Impersation of Electoral Officials/Entities`), + reason: '#reasonCivicImpersonation', + }, + { + title: _(msg`Other`), + reason: '#reasonCivicOther', + }, + ], + }, + other: { + key: 'other', 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: [ + sort: 8, + options: [ { - reason: ComAtprotoModerationDefs.REASONMISLEADING, - title: _(msg`Misleading Account`), - description: _( - msg`Impersonation or false claims about identity or affiliation`, - ), + title: _(msg`Other`), + reason: '#reasonOther', }, - { - 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, - ], - } - }, [_]) + isOther: true, + }, + } + + const sortedReasons = Object.values(reasons).sort((a, b) => a.sort - b.sort) + + const getTopLevelReason = (reasonName: TopLevelReason) => { + return reasons[reasonName] + } + + return {reasons, sortedReasons, getTopLevelReason} }