From c5872f66443145873baceb6bd55082defe8a260f Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Wed, 6 Mar 2024 10:43:08 -0800 Subject: [PATCH] Get report flow working again with temporarily fixed report options --- .../ReportDialog/SelectReportOptionView.tsx | 178 +++++++ src/components/ReportDialog/SubmitView.tsx | 238 +++++++++ src/components/ReportDialog/index.tsx | 489 +----------------- src/components/ReportDialog/types.ts | 15 + src/lib/moderation.ts | 15 - src/lib/moderation/useLabelGroupStrings.ts | 130 ----- src/lib/moderation/useReportOptions.ts | 85 +++ src/screens/Profile/Header/DropdownBtn.tsx | 2 +- src/state/queries/preferences/index.ts | 1 - src/state/queries/preferences/moderation.ts | 10 +- src/view/com/util/forms/PostDropdownBtn.tsx | 2 +- 11 files changed, 551 insertions(+), 614 deletions(-) create mode 100644 src/components/ReportDialog/SelectReportOptionView.tsx create mode 100644 src/components/ReportDialog/SubmitView.tsx create mode 100644 src/components/ReportDialog/types.ts delete mode 100644 src/lib/moderation/useLabelGroupStrings.ts create mode 100644 src/lib/moderation/useReportOptions.ts diff --git a/src/components/ReportDialog/SelectReportOptionView.tsx b/src/components/ReportDialog/SelectReportOptionView.tsx new file mode 100644 index 0000000000..b2e6f9f7b3 --- /dev/null +++ b/src/components/ReportDialog/SelectReportOptionView.tsx @@ -0,0 +1,178 @@ +import React from 'react' +import {View} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {AppBskyLabelerDefs} from '@atproto/api' + +import {useReportOptions, ReportOption} from '#/lib/moderation/useReportOptions' +import {DMCA_LINK} from '#/components/ReportDialog/const' +import {Link} from '#/components/Link' +export {useDialogControl as useReportDialogControl} from '#/components/Dialog' + +import {atoms as a, useTheme} from '#/alf' +import {Text} from '#/components/Typography' +import { + Button, + ButtonIcon, + ButtonText, + useButtonContext, +} from '#/components/Button' +import {Divider} from '#/components/Divider' +import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron' +import {SquareArrowTopRight_Stroke2_Corner0_Rounded as SquareArrowTopRight} from '#/components/icons/SquareArrowTopRight' + +import {ReportDialogProps} from './types' + +export function SelectReportOptionView({ + ...props +}: ReportDialogProps & { + labelers: AppBskyLabelerDefs.LabelerViewDetailed[] + onSelectReportOption: (reportOption: ReportOption) => void +}) { + const t = useTheme() + const {_} = useLingui() + const allReportOptions = useReportOptions() + const reportOptions = allReportOptions[props.params.type] + + const i18n = React.useMemo(() => { + let title = _(msg`Report this content`) + let description = _(msg`Why should this content be reviewed?`) + + if (props.params.type === 'account') { + title = _(msg`Report this user`) + description = _(msg`Why should this user be reviewed?`) + } else if (props.params.type === 'post') { + title = _(msg`Report this post`) + description = _(msg`Why should this post be reviewed?`) + } else if (props.params.type === 'list') { + title = _(msg`Report this list`) + description = _(msg`Why should this list be reviewed?`) + } + + return { + title, + description, + } + }, [_, props.params.type]) + + return ( + + + {i18n.title} + + {i18n.description} + + + + + + + {reportOptions.map(reportOption => { + return ( + + ) + })} + + {(props.params.type === 'post' || props.params.type === 'account') && ( + + + + Need to report a copyright violation? + + + View details + + + + + )} + + + ) +} + +function ReportOptionButton({ + title, + description, +}: { + title: string + description: string +}) { + const t = useTheme() + const {hovered, focused, pressed} = useButtonContext() + const interacted = hovered || focused || pressed + + const styles = React.useMemo(() => { + return { + interacted: { + backgroundColor: t.palette.contrast_50, + }, + } + }, [t]) + + return ( + + + + {title} + + {description} + + + + + + + ) +} diff --git a/src/components/ReportDialog/SubmitView.tsx b/src/components/ReportDialog/SubmitView.tsx new file mode 100644 index 0000000000..3d4e29026b --- /dev/null +++ b/src/components/ReportDialog/SubmitView.tsx @@ -0,0 +1,238 @@ +import React from 'react' +import {View} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {AppBskyLabelerDefs} from '@atproto/api' + +import {getModerationServiceTitle} from '#/lib/moderation' +import {ReportOption} from '#/lib/moderation/useReportOptions' + +import {atoms as a, useTheme, tokens, native} from '#/alf' +import {Text} from '#/components/Typography' +import * as Dialog from '#/components/Dialog' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron' +import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' +import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' +import {GradientFill} from '#/components/GradientFill' +import * as Toggle from '#/components/forms/Toggle' +import {CharProgress} from '#/view/com/composer/char-progress/CharProgress' +import {Loader} from '#/components/Loader' +import * as Toast from '#/view/com/util/Toast' + +import {ReportDialogProps} from './types' + +export function SubmitView({ + params, + labelers, + selectedReportOption, + goBack, + onSubmitComplete, +}: ReportDialogProps & { + labelers: AppBskyLabelerDefs.LabelerViewDetailed[] + selectedReportOption: ReportOption + goBack: () => void + onSubmitComplete: () => void +}) { + const t = useTheme() + const {_} = useLingui() + const [details, setDetails] = React.useState('') + const [submitting, setSubmitting] = React.useState(false) + const [selectedServices, setSelectedServices] = React.useState( + labelers?.map(labeler => labeler.creator.did) || [], + ) + + const submit = React.useCallback(async () => { + setSubmitting(true) + await new Promise(resolve => setTimeout(resolve, 1000)) + + const $type = + params.type === 'account' + ? 'com.atproto.admin.defs#repoRef' + : 'com.atproto.repo.strongRef' + const report = { + reasonType: selectedReportOption.reason, + subject: { + $type, + ...params, + }, + reason: details, + } + console.log(report) + // await getAgent().createModerationReport(report) + + setSubmitting(false) + + Toast.show(`Thank you. Your report has been sent.`) + + onSubmitComplete() + }, [params, details, selectedReportOption, onSubmitComplete]) + + return ( + + + + + + + {selectedReportOption.title} + + + {selectedReportOption.description} + + + + + + + + + Select the moderation service(s) to report to + + + + + {labelers.map(labeler => { + const title = getModerationServiceTitle({ + displayName: labeler.creator.displayName, + handle: labeler.creator.handle, + }) + return ( + + + + ) + })} + + + + + + Optionally provide additional information below: + + + + + + + + + + + + + {!selectedServices.length && ( + + You must select at least one labeler for a report + + )} + + + + + ) +} + +function LabelerToggle({title}: {title: string}) { + const t = useTheme() + const ctx = Toggle.useItemContext() + + return ( + + {ctx.selected && } + + + {title} + + + + + ) +} diff --git a/src/components/ReportDialog/index.tsx b/src/components/ReportDialog/index.tsx index 210cf45a71..ae17cb947e 100644 --- a/src/components/ReportDialog/index.tsx +++ b/src/components/ReportDialog/index.tsx @@ -1,498 +1,65 @@ import React from 'react' import {View, Linking, Pressable} from 'react-native' -import {msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {AppBskyLabelerDefs, LabelGroupDefinition} from '@atproto/api' +import {AppBskyLabelerDefs} from '@atproto/api' -import {atoms as a, useTheme, tokens, native} from '#/alf' -import {Text} from '#/components/Typography' -import * as Dialog from '#/components/Dialog' -import { - Button, - ButtonIcon, - ButtonText, - useButtonContext, -} from '#/components/Button' -import {Divider} from '#/components/Divider' -import {useLabelGroupStrings} from '#/lib/moderation/useLabelGroupStrings' -import { - ChevronRight_Stroke2_Corner0_Rounded as ChevronRight, - ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft, -} from '#/components/icons/Chevron' -import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' -import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' -import * as Toggle from '#/components/forms/Toggle' -import {GradientFill} from '#/components/GradientFill' -import {CharProgress} from '#/view/com/composer/char-progress/CharProgress' -import {Loader} from '#/components/Loader' -import * as Toast from '#/view/com/util/Toast' -import {usePreferencesQuery} from '#/state/queries/preferences' -import {useLabelersDetailedInfoQuery} from '#/state/queries/labeler' -import { - getModerationServiceTitle, - useConfigurableContentLabelGroups, - useConfigurableProfileLabelGroups, -} from '#/lib/moderation' +import {useMyLabelers} from '#/state/queries/preferences' +import {ReportOption} from '#/lib/moderation/useReportOptions' import {DMCA_LINK} from '#/components/ReportDialog/const' -import {Link} from '#/components/Link' -import {SquareArrowTopRight_Stroke2_Corner0_Rounded as SquareArrowTopRight} from '#/components/icons/SquareArrowTopRight' -// import {getAgent} from '#/state/session' - export {useDialogControl as useReportDialogControl} from '#/components/Dialog' -export type ReportDialogLabelIds = LabelGroupDefinition['id'] | 'other' -export type ReportDialogProps = { - control: Dialog.DialogOuterProps['control'] - params: - | { - type: 'content' - uri: string - cid: string - } - | { - type: 'profile' - did: string - } -} +import {atoms as a} from '#/alf' +import {Loader} from '#/components/Loader' +import * as Dialog from '#/components/Dialog' -function LabelGroupButton({ - name, - description, -}: { - name: string - description: string -}) { - const t = useTheme() - const {hovered, focused, pressed} = useButtonContext() - const interacted = hovered || focused || pressed - - const styles = React.useMemo(() => { - return { - interacted: { - backgroundColor: t.palette.contrast_50, - }, - } - }, [t]) - - return ( - - - - {name} - - {description} - - - - - - - ) -} - -function LabelerToggle({title}: {title: string}) { - const t = useTheme() - const ctx = Toggle.useItemContext() - - return ( - - {ctx.selected && } - - - {title} - - - - - ) -} - -function SubmitView({ - params, - selectedLabelGroup, - goBack, - onSubmitComplete, -}: ReportDialogProps & { - selectedLabelGroup: ReportDialogLabelIds - goBack: () => void - onSubmitComplete: () => void -}) { - const t = useTheme() - const {_} = useLingui() - const labelGroupStrings = useLabelGroupStrings() - const groupInfoStrings = labelGroupStrings[selectedLabelGroup] - const [details, setDetails] = React.useState('') - const [submitting, setSubmitting] = React.useState(false) - const supportedLabelers = [] //labelGroupToLabelerMap[selectedLabelGroup] - const [selectedServices, setSelectedServices] = React.useState( - supportedLabelers?.map(labeler => labeler.creator.did) || [], - ) - - const submit = React.useCallback(async () => { - setSubmitting(true) - await new Promise(resolve => setTimeout(resolve, 1000)) - - const $type = - params.type === 'content' - ? 'com.atproto.repo.strongRef' - : 'com.atproto.admin.defs#repoRef' - const report = { - reasonType: selectedLabelGroup, // TODO map to reasons - subject: { - $type, - ...params, - }, - reason: details, - } - console.log(report) - // await getAgent().createModerationReport(report) - - setSubmitting(false) - - Toast.show(`Thank you. Your report has been sent.`) - - onSubmitComplete() - }, [params, details, selectedLabelGroup, onSubmitComplete]) - - return ( - - - - - - {groupInfoStrings.name} - - {groupInfoStrings.description} - - - - - - - - - Select the moderation service(s) to report to - - - {supportedLabelers ? ( - - - {supportedLabelers.map(labeler => { - const title = getModerationServiceTitle({ - displayName: labeler.creator.displayName, - handle: labeler.creator.handle, - }) - return ( - - - - ) - })} - - - ) : ( - - - None of your subscribed labelers support this content type. - - - )} - - - - Optionally provide additional information below: - - - - - - - - - - - - - {!selectedServices.length && ( - - You must select at least one labeler for a report - - )} - - - - - ) -} +import {ReportDialogProps} from './types' +import {SelectReportOptionView} from './SelectReportOptionView' +import {SubmitView} from './SubmitView' export function ReportDialogLoaded({ - labelGroupToLabelerMap, ...props }: ReportDialogProps & { labelers: AppBskyLabelerDefs.LabelerViewDetailed[] - labelGroupToLabelerMap: ReturnType }) { - const t = useTheme() - const {_} = useLingui() const control = Dialog.useDialogControl() - const [selectedLabelGroup, setSelectedLabelGroup] = React.useState< - ReportDialogLabelIds | undefined + const [selectedReportOption, setSelectedReportOption] = React.useState< + ReportOption | undefined >() - const labelGroupStrings = useLabelGroupStrings() - const contentGroups = useConfigurableContentLabelGroups() - const profileGroups = useConfigurableProfileLabelGroups() - const groups = props.params.type === 'content' ? contentGroups : profileGroups - const filteredGroups = groups.filter(group => { - return Boolean(labelGroupToLabelerMap[group.id]) - }) - const i18n = React.useMemo(() => { - let title = _(msg`Report this post`) - let description = _(msg`Why should this post be reviewed?`) - - if (props.params.type === 'profile') { - title = _(msg`Report this user`) - description = _(msg`Why should this user be reviewed?`) - } - - return { - title, - description, - } - }, [_, props.params.type]) - - const next = React.useCallback( - (group: ReportDialogLabelIds | 'copyright') => { - if (group === 'copyright') { + const onSelectReportOption = React.useCallback( + (reportOption: ReportOption) => { + if (reportOption.reason === 'copyright') { Linking.openURL(DMCA_LINK) } else { - setSelectedLabelGroup(group) + setSelectedReportOption(reportOption) } }, - [setSelectedLabelGroup], + [setSelectedReportOption], ) return ( <> - {selectedLabelGroup ? ( + {selectedReportOption ? ( setSelectedLabelGroup(undefined)} + selectedReportOption={selectedReportOption} + goBack={() => setSelectedReportOption(undefined)} onSubmitComplete={control.close} /> ) : ( - - - {i18n.title} - - {i18n.description} - - - - - - - {filteredGroups.map(def => { - const strings = labelGroupStrings[def.id] - return ( - - ) - })} - - - - {props.params.type === 'content' && ( - - - - Need to report a copyright violation? - - - View details - - - - - )} - - + )} ) } function ReportDialogInner(props: ReportDialogProps) { - const { - isLoading: isPreferencesLoading, - error: preferencesError, - data: preferences, - } = usePreferencesQuery() - const { - isLoading: isLabelersLoading, - data: labelers, - error: labelersError, - } = useLabelersDetailedInfoQuery({ - dids: preferences ? preferences.moderationPrefs.mods.map(m => m.did) : [], - }) - const isLoading = isPreferencesLoading || isLabelersLoading - const error = preferencesError || labelersError + const {isLoading, data: labelers, error} = useMyLabelers() const [fakeLoading, setFakeLoading] = React.useState(isLoading) - const labelGroupToLabelerMap = React.useMemo(() => { - if (!labelers) return {} - return getLabelGroupToLabelerMap(labelers) - }, [labelers]) - React.useEffect(() => { // on initial load, show a loading spinner for a hot sec to prevent flash if (fakeLoading) setTimeout(() => setFakeLoading(false), 500) @@ -506,12 +73,8 @@ function ReportDialogInner(props: ReportDialogProps) { {/* Here to capture focus for a hot sec to prevent flash */} - ) : error || !(preferences && labelers) ? null : ( // TODO - + ) : error || !labelers ? null : ( // TODO + )} ) diff --git a/src/components/ReportDialog/types.ts b/src/components/ReportDialog/types.ts new file mode 100644 index 0000000000..6d441c7812 --- /dev/null +++ b/src/components/ReportDialog/types.ts @@ -0,0 +1,15 @@ +import * as Dialog from '#/components/Dialog' + +export type ReportDialogProps = { + control: Dialog.DialogOuterProps['control'] + params: + | { + type: 'post' | 'list' | 'other' + uri: string + cid: string + } + | { + type: 'account' + did: string + } +} diff --git a/src/lib/moderation.ts b/src/lib/moderation.ts index f70cdef9a7..d806581a3c 100644 --- a/src/lib/moderation.ts +++ b/src/lib/moderation.ts @@ -25,21 +25,6 @@ export function isJustAMute(modui: ModerationUI): boolean { return modui.filters.length === 1 && modui.filters[0].type === 'muted' } -export function useConfigurableContentLabelGroups() { - // TODO removeme - return [] -} - -export function useConfigurableProfileLabelGroups() { - // TODO removeme - return [] -} - -export function useConfigurableAccountLabelGroups() { - // TODO removeme - return [] -} - export function getModerationServiceTitle({ displayName, handle, diff --git a/src/lib/moderation/useLabelGroupStrings.ts b/src/lib/moderation/useLabelGroupStrings.ts deleted file mode 100644 index b858a1087d..0000000000 --- a/src/lib/moderation/useLabelGroupStrings.ts +++ /dev/null @@ -1,130 +0,0 @@ -import {LABEL_GROUPS} from '@atproto/api' -import {msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {useMemo} from 'react' - -export type LabelGroupStrings = Record< - keyof typeof LABEL_GROUPS | 'other', - {name: string; description: string} -> - -export function useLabelGroupStrings(): LabelGroupStrings { - const {_} = useLingui() - return useMemo( - () => ({ - system: { - name: _(msg`System`), - description: _(msg`Moderator overrides for special cases.`), - }, - legal: { - name: _(msg`Legal`), - description: _(msg`Content removed for legal reasons.`), - }, - 'intellectual-property': { - name: _(msg`Intellectual Property`), - description: _(msg`Plagiarism, copying without attribution.`), - }, - porn: { - name: _(msg`Explicit Sexual Images`), - description: _(msg`i.e. pornography.`), - }, - suggestive: { - name: _(msg`Sexually Suggestive`), - description: _(msg`Does not include nudity.`), - }, - nudity: { - name: _(msg`Other Nudity`), - description: _(msg`Including non-sexual and artistic.`), - }, - violence: { - name: _(msg`Violent / Bloody`), - description: _(msg`Gore, self-harm, torture.`), - }, - 'drugs-alcohol': { - name: _(msg`Substance Abuse`), - description: _(msg`Use of drugs or alcohol.`), - }, - 'self-harm': { - name: _(msg`Self Harm`), - description: _(msg`Suicide, self-harm, eating disorders.`), - }, - intolerance: { - name: _(msg`Intolerance`), - description: _( - msg`Content or behavior which is hateful or intolerant toward a group of people.`, - ), - }, - 'bad-behavior': { - name: _(msg`Bad Behavior`), - description: _( - msg`Harassment, bullying, and threats toward other users.`, - ), - }, - rude: { - name: _(msg`Rude`), - description: _(msg`Behavior which is rude toward other users.`), - }, - upsetting: { - name: _(msg`Upsetting`), - description: _( - msg`Shocking, disgusting, or generally upsetting content.`, - ), - }, - troubling: { - name: _(msg`Troubling`), - description: _( - msg`Bad news, troubling information, or dispiriting content.`, - ), - }, - 'hate-group-mention': { - name: _(msg`Hate Group Coverage`), - description: _( - msg`Images of terror groups, articles covering events, etc.`, - ), - }, - discourse: { - name: _(msg`Discourse / Drama`), - description: _( - msg`On-going discussions or debates that may be frustrating.`, - ), - }, - curation: { - name: _(msg`Curation`), - description: _( - msg`Judgment of the moderators to remove content not worth showing.`, - ), - }, - spam: { - name: _(msg`Spam`), - description: _(msg`Content which doesn't add to the conversation.`), - }, - misrepresentation: { - name: _(msg`Misrepresentation`), - description: _(msg`Impersonations, scams.`), - }, - security: { - name: _(msg`Security`), - description: _(msg`Potential security attacks.`), - }, - misinfo: { - name: _(msg`Misinformation`), - description: _(msg`Content which misleads or defrauds users.`), - }, - context: { - name: _(msg`Context`), - description: _( - msg`Helpful annotations to explain intent, such as satire or parody.`, - ), - }, - bot: { - name: _(msg`Bots`), - description: _(msg`Automated accounts which follow the rules.`), - }, - other: { - name: _(msg`Other`), - description: _(msg`Other content not covered by the other categories.`), - }, - }), - [_], - ) -} diff --git a/src/lib/moderation/useReportOptions.ts b/src/lib/moderation/useReportOptions.ts new file mode 100644 index 0000000000..8c1afe4578 --- /dev/null +++ b/src/lib/moderation/useReportOptions.ts @@ -0,0 +1,85 @@ +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useMemo} from 'react' +import {ComAtprotoModerationDefs} from '@atproto/api' + +export interface ReportOption { + reason: string + title: string + description: string +} + +interface ReportOptions { + account: ReportOption[] + post: ReportOption[] + list: ReportOption[] + other: 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.REASONSPAM, + title: _(msg`Spam`), + description: _(msg`Excessive mentions or replies`), + }, + { + reason: ComAtprotoModerationDefs.REASONSEXUAL, + title: _(msg`Unwanted Sexual Content`), + description: _(msg`Nudity or pornography not labeled as such`), + }, + ...common, + ], + list: [ + { + reason: ComAtprotoModerationDefs.REASONVIOLATION, + title: _(msg`Name or Description Violates Community Standards`), + description: _(msg`Terms used violate community standards`), + }, + ...common, + ], + other: common, + } + }, [_]) +} diff --git a/src/screens/Profile/Header/DropdownBtn.tsx b/src/screens/Profile/Header/DropdownBtn.tsx index 8e5b3b5619..fe66e99d62 100644 --- a/src/screens/Profile/Header/DropdownBtn.tsx +++ b/src/screens/Profile/Header/DropdownBtn.tsx @@ -270,7 +270,7 @@ export function ProfileHeaderDropdownBtn({ <> [ + (labelers.data || []).map(labeler => [ labeler.creator.did, interpretLabelValueDefinitions(labeler), ]), ), - labelers, + labelers: labelers.data || [], } } diff --git a/src/view/com/util/forms/PostDropdownBtn.tsx b/src/view/com/util/forms/PostDropdownBtn.tsx index e6cfd9ba8a..ccfaff301d 100644 --- a/src/view/com/util/forms/PostDropdownBtn.tsx +++ b/src/view/com/util/forms/PostDropdownBtn.tsx @@ -284,7 +284,7 @@ let PostDropdownBtn = ({