From 16db93d8f9b378ed39cecf1fde56cfcce9c797da Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 15 Feb 2024 09:53:29 -0600 Subject: [PATCH 1/4] Fix toggles --- src/screens/Moderation/SettingsDialog.tsx | 6 ++---- src/screens/ProfileModerationService/PreferenceRow.tsx | 8 ++++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/screens/Moderation/SettingsDialog.tsx b/src/screens/Moderation/SettingsDialog.tsx index 7c7b905585..11c7596cea 100644 --- a/src/screens/Moderation/SettingsDialog.tsx +++ b/src/screens/Moderation/SettingsDialog.tsx @@ -24,8 +24,7 @@ function LabelerToggle({ preferences: UsePreferencesQueryResponse }) { const t = useTheme() - const {mutateAsync, variables, reset} = - useModServiceLabelGroupEnableMutation() + const {mutateAsync, variables} = useModServiceLabelGroupEnableMutation() const modservicePreferences = preferences.moderationOpts.mods.find( ({did}) => did === labeler.creator.did, @@ -46,12 +45,11 @@ function LabelerToggle({ group: labelGroup, enabled: !enabled, }) - reset() // Important: clears query `variables` } catch (e: any) { // TODO console.error(e) } - }, [mutateAsync, enabled, modservicePreferences, labelGroup, reset]) + }, [mutateAsync, enabled, modservicePreferences, labelGroup]) return ( + label={_(msg`Enable`)}> {enabled ? 'Enabled' : 'Disabled'} From b52beefa27bf473e9e9d2e9995617ca20e57a1bd Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 15 Feb 2024 10:21:48 -0600 Subject: [PATCH 2/4] Add copyright and other option to report --- ...eArrowTopRight_stroke2_corner0_rounded.svg | 1 + src/components/dialogs/ReportDialog/const.ts | 1 + src/components/dialogs/ReportDialog/index.tsx | 104 +++++++++++++----- src/components/icons/SquareArrowTopRight.tsx | 5 + src/lib/moderation/useLabelGroupStrings.ts | 6 +- 5 files changed, 90 insertions(+), 27 deletions(-) create mode 100644 assets/icons/squareArrowTopRight_stroke2_corner0_rounded.svg create mode 100644 src/components/dialogs/ReportDialog/const.ts create mode 100644 src/components/icons/SquareArrowTopRight.tsx diff --git a/assets/icons/squareArrowTopRight_stroke2_corner0_rounded.svg b/assets/icons/squareArrowTopRight_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..1407a1d6fe --- /dev/null +++ b/assets/icons/squareArrowTopRight_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/src/components/dialogs/ReportDialog/const.ts b/src/components/dialogs/ReportDialog/const.ts new file mode 100644 index 0000000000..30c9aff88d --- /dev/null +++ b/src/components/dialogs/ReportDialog/const.ts @@ -0,0 +1 @@ +export const DMCA_LINK = 'https://bsky.social/about/support/copyright' diff --git a/src/components/dialogs/ReportDialog/index.tsx b/src/components/dialogs/ReportDialog/index.tsx index 4868d552d6..69385b303b 100644 --- a/src/components/dialogs/ReportDialog/index.tsx +++ b/src/components/dialogs/ReportDialog/index.tsx @@ -1,5 +1,5 @@ import React from 'react' -import {View, Dimensions} from 'react-native' +import {View, Dimensions, Linking} from 'react-native' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {AppBskyModerationDefs, LabelGroupDefinition} from '@atproto/api' @@ -35,7 +35,11 @@ import { getModerationServiceTitle, useConfigurableLabelGroups, } from '#/lib/moderation' +import {DMCA_LINK} from '#/components/dialogs/ReportDialog/const' +import {Link} from '#/components/Link' +import {SquareArrowTopRight_Stroke2_Corner0_Rounded as SquareArrowTopRight} from '#/components/icons/SquareArrowTopRight' +export type ReportDialogLabelIds = LabelGroupDefinition['id'] | 'other' export type ReportDialogProps = | { type: 'post' @@ -48,14 +52,14 @@ export type ReportDialogProps = } function LabelGroupButton({ - labelGroup, + name, + description, }: { - labelGroup: LabelGroupDefinition['id'] + name: string + description: string }) { const t = useTheme() const {hovered, focused, pressed} = useButtonContext() - const labelGroupStrings = useLabelGroupStrings() - const groupInfoStrings = labelGroupStrings[labelGroup] const interacted = hovered || focused || pressed const styles = React.useMemo(() => { @@ -80,11 +84,9 @@ function LabelGroupButton({ ]}> - {groupInfoStrings.name} - - - {groupInfoStrings.description} + {name} + {description} - > = {} + > = { + // report `other` to all labelers + other: modservices, + } for (const modservice of modservices) { const labelGroups = getLabelGroupsFromLabels( @@ -219,12 +224,12 @@ function SubmitView({ onSubmitComplete, labelGroupToModServiceMap, }: { - selectedLabelGroup: LabelGroupDefinition['id'] + selectedLabelGroup: ReportDialogLabelIds goBack: () => void onSubmitComplete: () => void labelers: AppBskyModerationDefs.ModServiceViewDetailed[] labelGroupToModServiceMap: Record< - LabelGroupDefinition['id'], + ReportDialogLabelIds, AppBskyModerationDefs.ModServiceViewDetailed[] > }) { @@ -364,25 +369,22 @@ function SubmitView({ ) } -/** - * TODO copyright link out to DMCA - * TODO add "other" option - */ export function ReportDialog({ params, cleanup, }: GlobalDialogProps) { + // REQUIRED CLEANUP + const onClose = React.useCallback(() => cleanup(), [cleanup]) + const t = useTheme() const {_} = useLingui() const insets = useSafeAreaInsets() const control = Dialog.useDialogControl() const [selectedLabelGroup, setSelectedLabelGroup] = React.useState< - LabelGroupDefinition['id'] | undefined + ReportDialogLabelIds | undefined >() const labelGroupStrings = useLabelGroupStrings() - - // REQUIRED CLEANUP - const onClose = React.useCallback(() => cleanup(), [cleanup]) + const groups = useConfigurableLabelGroups() const i18n = React.useMemo(() => { let title = _(msg`Report this post`) @@ -399,7 +401,16 @@ export function ReportDialog({ } }, [_, params.type]) - const groups = useConfigurableLabelGroups() + const next = React.useCallback( + (group: ReportDialogLabelIds | 'copyright') => { + if (group === 'copyright') { + Linking.openURL(DMCA_LINK) + } else { + setSelectedLabelGroup(group) + } + }, + [setSelectedLabelGroup], + ) return ( {groups.map(def => { - const groupStrings = labelGroupStrings[def.id] + const strings = labelGroupStrings[def.id] return ( ) })} + + + + + + + Need to report a copyright violation? + + + View details + + + + )} diff --git a/src/components/icons/SquareArrowTopRight.tsx b/src/components/icons/SquareArrowTopRight.tsx new file mode 100644 index 0000000000..7701e26e56 --- /dev/null +++ b/src/components/icons/SquareArrowTopRight.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const SquareArrowTopRight_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M14 5a1 1 0 1 1 0-2h6a1 1 0 0 1 1 1v6a1 1 0 1 1-2 0V6.414l-7.293 7.293a1 1 0 0 1-1.414-1.414L17.586 5H14ZM3 6a1 1 0 0 1 1-1h5a1 1 0 0 1 0 2H5v12h12v-4a1 1 0 1 1 2 0v5a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V6Z', +}) diff --git a/src/lib/moderation/useLabelGroupStrings.ts b/src/lib/moderation/useLabelGroupStrings.ts index ebd33fc0c7..152c8010f3 100644 --- a/src/lib/moderation/useLabelGroupStrings.ts +++ b/src/lib/moderation/useLabelGroupStrings.ts @@ -4,7 +4,7 @@ import {useLingui} from '@lingui/react' import {useMemo} from 'react' export type LabelGroupStrings = Record< - keyof typeof LABEL_GROUPS, + keyof typeof LABEL_GROUPS | 'other', {name: string; description: string} > @@ -116,6 +116,10 @@ export function useLabelGroupStrings(): LabelGroupStrings { msg`Helpful annotations to explain intent, such as satire or parody.`, ), }, + other: { + name: _(msg`Other`), + description: _(msg`Other content not covered by the other categories.`), + }, }), [_], ) From b73f9710f9fb8de4d9fc03c9fcf12f1493035b2c Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 15 Feb 2024 10:37:34 -0600 Subject: [PATCH 3/4] Handle reports on profile vs content --- src/components/dialogs/ReportDialog/index.tsx | 69 ++++++++++--------- src/lib/moderation.ts | 27 ++++++++ src/view/com/profile/ProfileHeader.tsx | 18 +++-- src/view/com/util/forms/PostDropdownBtn.tsx | 6 +- 4 files changed, 82 insertions(+), 38 deletions(-) diff --git a/src/components/dialogs/ReportDialog/index.tsx b/src/components/dialogs/ReportDialog/index.tsx index 69385b303b..f89a1e1624 100644 --- a/src/components/dialogs/ReportDialog/index.tsx +++ b/src/components/dialogs/ReportDialog/index.tsx @@ -33,7 +33,8 @@ import {useModServicesDetailedInfoQuery} from '#/state/queries/modservice' import { getLabelGroupsFromLabels, getModerationServiceTitle, - useConfigurableLabelGroups, + useConfigurableContentLabelGroups, + useConfigurableProfileLabelGroups, } from '#/lib/moderation' import {DMCA_LINK} from '#/components/dialogs/ReportDialog/const' import {Link} from '#/components/Link' @@ -42,12 +43,12 @@ import {SquareArrowTopRight_Stroke2_Corner0_Rounded as SquareArrowTopRight} from export type ReportDialogLabelIds = LabelGroupDefinition['id'] | 'other' export type ReportDialogProps = | { - type: 'post' + type: 'content' uri: string cid: string } | { - type: 'user' + type: 'profile' did: string } @@ -384,13 +385,15 @@ export function ReportDialog({ ReportDialogLabelIds | undefined >() const labelGroupStrings = useLabelGroupStrings() - const groups = useConfigurableLabelGroups() + const contentGroups = useConfigurableContentLabelGroups() + const profileGroups = useConfigurableProfileLabelGroups() + const groups = params.type === 'content' ? contentGroups : profileGroups const i18n = React.useMemo(() => { let title = _(msg`Report this post`) let description = _(msg`Why should this post be reviewed?`) - if (params.type === 'user') { + if (params.type === 'profile') { title = _(msg`Report this user`) description = _(msg`Why should this user be reviewed?`) } @@ -474,34 +477,36 @@ export function ReportDialog({ /> - - - - Need to report a copyright violation? - - - View details - - + {params.type === 'content' && ( + + + + Need to report a copyright violation? + + + View details + + + - + )} )} diff --git a/src/lib/moderation.ts b/src/lib/moderation.ts index 62fb16d52b..87875a862c 100644 --- a/src/lib/moderation.ts +++ b/src/lib/moderation.ts @@ -39,6 +39,33 @@ export function useConfigurableLabelGroups() { return React.useMemo(() => getConfigurableLabelGroups(), []) } +export function useConfigurableContentLabelGroups() { + return React.useMemo(() => { + const groups = getConfigurableLabelGroups() + return groups.filter(group => { + return group.labels.every(l => l.targets.includes('content')) + }) + }, []) +} + +export function useConfigurableProfileLabelGroups() { + return React.useMemo(() => { + const groups = getConfigurableLabelGroups() + return groups.filter(group => { + return group.labels.every(l => l.targets.includes('profile')) + }) + }, []) +} + +export function useConfigurableAccountLabelGroups() { + return React.useMemo(() => { + const groups = getConfigurableLabelGroups() + return groups.filter(group => { + return group.labels.every(l => l.targets.includes('account')) + }) + }, []) +} + export function getModerationServiceTitle({ displayName, handle, diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx index 4579df7e31..de1b3decea 100644 --- a/src/view/com/profile/ProfileHeader.tsx +++ b/src/view/com/profile/ProfileHeader.tsx @@ -58,6 +58,9 @@ import {LabelInfo} from '../util/moderation/LabelInfo' import {useProfileShadow} from 'state/cache/profile-shadow' import * as ModerationServiceCard from '#/components/ModerationServiceCard' import {getModerationServiceTitle} from '#/lib/moderation' +import {useOpenGlobalDialog} from '#/components/dialogs' +import {ReportDialog} from '#/components/dialogs/ReportDialog' +import {NEW_REPORT_DIALOG_ENABLED} from '#/lib/build-flags' import {useTheme} from '#/alf' @@ -119,6 +122,7 @@ let ProfileHeader = ({ () => moderateProfile(profile, moderationOpts), [profile, moderationOpts], ) + const openDialog = useOpenGlobalDialog() const invalidateProfileQuery = React.useCallback(() => { queryClient.invalidateQueries({ @@ -280,11 +284,15 @@ let ProfileHeader = ({ const onPressReportAccount = React.useCallback(() => { track('ProfileHeader:ReportAccountButtonClicked') - openModal({ - name: 'report', - did: profile.did, - }) - }, [track, openModal, profile]) + if (NEW_REPORT_DIALOG_ENABLED) { + openDialog(ReportDialog, {type: 'profile', did: profile.did}) + } else { + openModal({ + name: 'report', + did: profile.did, + }) + } + }, [track, openModal, profile, openDialog]) const isMe = React.useMemo( () => currentAccount?.did === profile.did, diff --git a/src/view/com/util/forms/PostDropdownBtn.tsx b/src/view/com/util/forms/PostDropdownBtn.tsx index edb21f2380..616108ecef 100644 --- a/src/view/com/util/forms/PostDropdownBtn.tsx +++ b/src/view/com/util/forms/PostDropdownBtn.tsx @@ -214,7 +214,11 @@ let PostDropdownBtn = ({ label: _(msg`Report post`), onPress() { if (NEW_REPORT_DIALOG_ENABLED) { - openDialog(ReportDialog, {type: 'post', uri: postUri, cid: postCid}) + openDialog(ReportDialog, { + type: 'content', + uri: postUri, + cid: postCid, + }) } else { openModal({ name: 'report', From f255d7a48352b995864d566825abf21e1ef768e5 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 15 Feb 2024 11:14:38 -0600 Subject: [PATCH 4/4] Little cleanup --- src/components/dialogs/ReportDialog/index.tsx | 58 ++++++++----------- src/lib/moderation.ts | 36 +++++++++++- 2 files changed, 60 insertions(+), 34 deletions(-) diff --git a/src/components/dialogs/ReportDialog/index.tsx b/src/components/dialogs/ReportDialog/index.tsx index f89a1e1624..aac84c3992 100644 --- a/src/components/dialogs/ReportDialog/index.tsx +++ b/src/components/dialogs/ReportDialog/index.tsx @@ -31,14 +31,15 @@ import * as Toast from '#/view/com/util/Toast' import {usePreferencesQuery} from '#/state/queries/preferences' import {useModServicesDetailedInfoQuery} from '#/state/queries/modservice' import { - getLabelGroupsFromLabels, getModerationServiceTitle, useConfigurableContentLabelGroups, useConfigurableProfileLabelGroups, + getLabelGroupToLabelerMap, } from '#/lib/moderation' import {DMCA_LINK} from '#/components/dialogs/ReportDialog/const' import {Link} from '#/components/Link' import {SquareArrowTopRight_Stroke2_Corner0_Rounded as SquareArrowTopRight} from '#/components/icons/SquareArrowTopRight' +// import {getAgent} from '#/state/session' export type ReportDialogLabelIds = LabelGroupDefinition['id'] | 'other' export type ReportDialogProps = @@ -161,7 +162,7 @@ function SubmitViewLoader({ }: { children: (props: { labelers: AppBskyModerationDefs.ModServiceViewDetailed[] - labelGroupToModServiceMap: Record + labelGroupToModServiceMap: ReturnType }) => React.ReactNode }) { const { @@ -178,28 +179,7 @@ function SubmitViewLoader({ }) const labelGroupToModServiceMap = React.useMemo(() => { if (!modservices) return {} - - const groups: Partial< - Record< - ReportDialogLabelIds, - AppBskyModerationDefs.ModServiceViewDetailed[] - > - > = { - // report `other` to all labelers - other: modservices, - } - - for (const modservice of modservices) { - const labelGroups = getLabelGroupsFromLabels( - modservice.policies.labelValues, - ) - for (const group of labelGroups) { - const g = (groups[group.id] = groups[group.id] || []) - g.push(modservice) - } - } - - return groups + return getLabelGroupToLabelerMap(modservices) }, [modservices]) const isLoading = isPreferencesLoading || isModServicesLoading @@ -212,27 +192,24 @@ function SubmitViewLoader({ ) : error || !(preferences && modservices) ? null : ( // TODO children({ labelers: modservices, - // TODO mismatched types - // @ts-ignore labelGroupToModServiceMap, }) ) } function SubmitView({ + params, selectedLabelGroup, goBack, onSubmitComplete, labelGroupToModServiceMap, }: { + params: ReportDialogProps selectedLabelGroup: ReportDialogLabelIds goBack: () => void onSubmitComplete: () => void labelers: AppBskyModerationDefs.ModServiceViewDetailed[] - labelGroupToModServiceMap: Record< - ReportDialogLabelIds, - AppBskyModerationDefs.ModServiceViewDetailed[] - > + labelGroupToModServiceMap: ReturnType }) { const t = useTheme() const {_} = useLingui() @@ -246,12 +223,28 @@ function SubmitView({ 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() - }, [onSubmitComplete]) + }, [params, details, selectedLabelGroup, onSubmitComplete]) return ( @@ -431,10 +424,9 @@ export function ReportDialog({ {selectedLabelGroup ? ( {props => ( - // TODO same types mismatch - // @ts-ignore setSelectedLabelGroup(undefined)} onSubmitComplete={control.close} diff --git a/src/lib/moderation.ts b/src/lib/moderation.ts index 87875a862c..1a588a6fe7 100644 --- a/src/lib/moderation.ts +++ b/src/lib/moderation.ts @@ -1,5 +1,11 @@ import React from 'react' -import {ModerationCause, LABEL_GROUPS, LabelGroupDefinition} from '@atproto/api' +import { + ModerationCause, + LABEL_GROUPS, + LabelGroupDefinition, + AppBskyModerationDefs, +} from '@atproto/api' + import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' @@ -77,3 +83,31 @@ export function getModerationServiceTitle({ ? sanitizeDisplayName(displayName) : sanitizeHandle(handle, '@') } + +export function getLabelGroupToLabelerMap( + labelers: AppBskyModerationDefs.ModServiceViewDetailed[], +) { + if (!labelers) return {} + + const groups: Partial< + Record< + LabelGroupDefinition['id'] | 'other', + AppBskyModerationDefs.ModServiceViewDetailed[] + > + > = { + // `other` reports go to all labelers TODO confirm this + other: labelers, + } + + for (const modservice of labelers) { + const labelGroups = getLabelGroupsFromLabels( + modservice.policies.labelValues, + ) + for (const group of labelGroups) { + const g = (groups[group.id] = groups[group.id] || []) + g.push(modservice) + } + } + + return groups +}