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..aac84c3992 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'
@@ -31,31 +31,37 @@ import * as Toast from '#/view/com/util/Toast'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {useModServicesDetailedInfoQuery} from '#/state/queries/modservice'
import {
- getLabelGroupsFromLabels,
getModerationServiceTitle,
- useConfigurableLabelGroups,
+ 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 =
| {
- type: 'post'
+ type: 'content'
uri: string
cid: string
}
| {
- type: 'user'
+ type: 'profile'
did: string
}
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 +86,9 @@ function LabelGroupButton({
]}>
- {groupInfoStrings.name}
-
-
- {groupInfoStrings.description}
+ {name}
+ {description}
+ labelGroupToModServiceMap: ReturnType
}) => React.ReactNode
}) {
const {
@@ -175,25 +179,7 @@ function SubmitViewLoader({
})
const labelGroupToModServiceMap = React.useMemo(() => {
if (!modservices) return {}
-
- const groups: Partial<
- Record<
- LabelGroupDefinition['id'],
- AppBskyModerationDefs.ModServiceViewDetailed[]
- >
- > = {}
-
- 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
@@ -206,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,
}: {
- selectedLabelGroup: LabelGroupDefinition['id']
+ params: ReportDialogProps
+ selectedLabelGroup: ReportDialogLabelIds
goBack: () => void
onSubmitComplete: () => void
labelers: AppBskyModerationDefs.ModServiceViewDetailed[]
- labelGroupToModServiceMap: Record<
- LabelGroupDefinition['id'],
- AppBskyModerationDefs.ModServiceViewDetailed[]
- >
+ labelGroupToModServiceMap: ReturnType
}) {
const t = useTheme()
const {_} = useLingui()
@@ -240,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 (
@@ -364,31 +363,30 @@ 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 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?`)
}
@@ -399,7 +397,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 (
{props => (
- // TODO same types mismatch
- // @ts-ignore
setSelectedLabelGroup(undefined)}
onSubmitComplete={control.close}
@@ -440,16 +446,59 @@ export function ReportDialog({
{groups.map(def => {
- const groupStrings = labelGroupStrings[def.id]
+ const strings = labelGroupStrings[def.id]
return (
)
})}
+
+
+
+ {params.type === 'content' && (
+
+
+
+ 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.ts b/src/lib/moderation.ts
index 62fb16d52b..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'
@@ -39,6 +45,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,
@@ -50,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
+}
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.`),
+ },
}),
[_],
)
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'}
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',