Merge branch '3p-moderators' of github.com:bluesky-social/social-app into 3p-moderators

This commit is contained in:
Paul Frazee
2024-02-15 11:13:27 -08:00
10 changed files with 204 additions and 73 deletions
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="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" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 342 B

@@ -0,0 +1 @@
export const DMCA_LINK = 'https://bsky.social/about/support/copyright'
+106 -57
View File
@@ -1,5 +1,5 @@
import React from 'react' import React from 'react'
import {View, Dimensions} from 'react-native' import {View, Dimensions, Linking} from 'react-native'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {AppBskyModerationDefs, LabelGroupDefinition} from '@atproto/api' 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 {usePreferencesQuery} from '#/state/queries/preferences'
import {useModServicesDetailedInfoQuery} from '#/state/queries/modservice' import {useModServicesDetailedInfoQuery} from '#/state/queries/modservice'
import { import {
getLabelGroupsFromLabels,
getModerationServiceTitle, getModerationServiceTitle,
useConfigurableLabelGroups, useConfigurableContentLabelGroups,
useConfigurableProfileLabelGroups,
getLabelGroupToLabelerMap,
} from '#/lib/moderation' } 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 = export type ReportDialogProps =
| { | {
type: 'post' type: 'content'
uri: string uri: string
cid: string cid: string
} }
| { | {
type: 'user' type: 'profile'
did: string did: string
} }
function LabelGroupButton({ function LabelGroupButton({
labelGroup, name,
description,
}: { }: {
labelGroup: LabelGroupDefinition['id'] name: string
description: string
}) { }) {
const t = useTheme() const t = useTheme()
const {hovered, focused, pressed} = useButtonContext() const {hovered, focused, pressed} = useButtonContext()
const labelGroupStrings = useLabelGroupStrings()
const groupInfoStrings = labelGroupStrings[labelGroup]
const interacted = hovered || focused || pressed const interacted = hovered || focused || pressed
const styles = React.useMemo(() => { const styles = React.useMemo(() => {
@@ -80,11 +86,9 @@ function LabelGroupButton({
]}> ]}>
<View style={[a.flex_1, a.gap_xs]}> <View style={[a.flex_1, a.gap_xs]}>
<Text style={[a.text_md, a.font_bold, t.atoms.text_contrast_medium]}> <Text style={[a.text_md, a.font_bold, t.atoms.text_contrast_medium]}>
{groupInfoStrings.name} {name}
</Text>
<Text style={[a.leading_tight, {maxWidth: 400}]}>
{groupInfoStrings.description}
</Text> </Text>
<Text style={[a.leading_tight, {maxWidth: 400}]}>{description}</Text>
</View> </View>
<View <View
@@ -158,7 +162,7 @@ function SubmitViewLoader({
}: { }: {
children: (props: { children: (props: {
labelers: AppBskyModerationDefs.ModServiceViewDetailed[] labelers: AppBskyModerationDefs.ModServiceViewDetailed[]
labelGroupToModServiceMap: Record<LabelGroupDefinition['id'], string[]> labelGroupToModServiceMap: ReturnType<typeof getLabelGroupToLabelerMap>
}) => React.ReactNode }) => React.ReactNode
}) { }) {
const { const {
@@ -175,25 +179,7 @@ function SubmitViewLoader({
}) })
const labelGroupToModServiceMap = React.useMemo(() => { const labelGroupToModServiceMap = React.useMemo(() => {
if (!modservices) return {} if (!modservices) return {}
return getLabelGroupToLabelerMap(modservices)
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
}, [modservices]) }, [modservices])
const isLoading = isPreferencesLoading || isModServicesLoading const isLoading = isPreferencesLoading || isModServicesLoading
@@ -206,27 +192,24 @@ function SubmitViewLoader({
) : error || !(preferences && modservices) ? null : ( // TODO ) : error || !(preferences && modservices) ? null : ( // TODO
children({ children({
labelers: modservices, labelers: modservices,
// TODO mismatched types
// @ts-ignore
labelGroupToModServiceMap, labelGroupToModServiceMap,
}) })
) )
} }
function SubmitView({ function SubmitView({
params,
selectedLabelGroup, selectedLabelGroup,
goBack, goBack,
onSubmitComplete, onSubmitComplete,
labelGroupToModServiceMap, labelGroupToModServiceMap,
}: { }: {
selectedLabelGroup: LabelGroupDefinition['id'] params: ReportDialogProps
selectedLabelGroup: ReportDialogLabelIds
goBack: () => void goBack: () => void
onSubmitComplete: () => void onSubmitComplete: () => void
labelers: AppBskyModerationDefs.ModServiceViewDetailed[] labelers: AppBskyModerationDefs.ModServiceViewDetailed[]
labelGroupToModServiceMap: Record< labelGroupToModServiceMap: ReturnType<typeof getLabelGroupToLabelerMap>
LabelGroupDefinition['id'],
AppBskyModerationDefs.ModServiceViewDetailed[]
>
}) { }) {
const t = useTheme() const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
@@ -240,12 +223,28 @@ function SubmitView({
const submit = React.useCallback(async () => { const submit = React.useCallback(async () => {
setSubmitting(true) setSubmitting(true)
await new Promise(resolve => setTimeout(resolve, 1000)) 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) setSubmitting(false)
Toast.show(`Thank you. Your report has been sent.`) Toast.show(`Thank you. Your report has been sent.`)
onSubmitComplete() onSubmitComplete()
}, [onSubmitComplete]) }, [params, details, selectedLabelGroup, onSubmitComplete])
return ( return (
<View style={[a.gap_2xl]}> <View style={[a.gap_2xl]}>
@@ -364,31 +363,30 @@ function SubmitView({
) )
} }
/**
* TODO copyright link out to DMCA
* TODO add "other" option
*/
export function ReportDialog({ export function ReportDialog({
params, params,
cleanup, cleanup,
}: GlobalDialogProps<ReportDialogProps>) { }: GlobalDialogProps<ReportDialogProps>) {
// REQUIRED CLEANUP
const onClose = React.useCallback(() => cleanup(), [cleanup])
const t = useTheme() const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
const insets = useSafeAreaInsets() const insets = useSafeAreaInsets()
const control = Dialog.useDialogControl() const control = Dialog.useDialogControl()
const [selectedLabelGroup, setSelectedLabelGroup] = React.useState< const [selectedLabelGroup, setSelectedLabelGroup] = React.useState<
LabelGroupDefinition['id'] | undefined ReportDialogLabelIds | undefined
>() >()
const labelGroupStrings = useLabelGroupStrings() const labelGroupStrings = useLabelGroupStrings()
const contentGroups = useConfigurableContentLabelGroups()
// REQUIRED CLEANUP const profileGroups = useConfigurableProfileLabelGroups()
const onClose = React.useCallback(() => cleanup(), [cleanup]) const groups = params.type === 'content' ? contentGroups : profileGroups
const i18n = React.useMemo(() => { const i18n = React.useMemo(() => {
let title = _(msg`Report this post`) let title = _(msg`Report this post`)
let description = _(msg`Why should this post be reviewed?`) let description = _(msg`Why should this post be reviewed?`)
if (params.type === 'user') { if (params.type === 'profile') {
title = _(msg`Report this user`) title = _(msg`Report this user`)
description = _(msg`Why should this user be reviewed?`) description = _(msg`Why should this user be reviewed?`)
} }
@@ -399,7 +397,16 @@ export function ReportDialog({
} }
}, [_, params.type]) }, [_, params.type])
const groups = useConfigurableLabelGroups() const next = React.useCallback(
(group: ReportDialogLabelIds | 'copyright') => {
if (group === 'copyright') {
Linking.openURL(DMCA_LINK)
} else {
setSelectedLabelGroup(group)
}
},
[setSelectedLabelGroup],
)
return ( return (
<Dialog.Outer <Dialog.Outer
@@ -417,10 +424,9 @@ export function ReportDialog({
{selectedLabelGroup ? ( {selectedLabelGroup ? (
<SubmitViewLoader> <SubmitViewLoader>
{props => ( {props => (
// TODO same types mismatch
// @ts-ignore
<SubmitView <SubmitView
{...props} {...props}
params={params}
selectedLabelGroup={selectedLabelGroup} selectedLabelGroup={selectedLabelGroup}
goBack={() => setSelectedLabelGroup(undefined)} goBack={() => setSelectedLabelGroup(undefined)}
onSubmitComplete={control.close} onSubmitComplete={control.close}
@@ -440,16 +446,59 @@ export function ReportDialog({
<View style={[a.gap_sm, {marginHorizontal: a.p_md.padding * -1}]}> <View style={[a.gap_sm, {marginHorizontal: a.p_md.padding * -1}]}>
{groups.map(def => { {groups.map(def => {
const groupStrings = labelGroupStrings[def.id] const strings = labelGroupStrings[def.id]
return ( return (
<Button <Button
key={def.id} key={def.id}
label={_(msg`Create report for ${groupStrings.name}`)} label={_(msg`Create report for ${strings.name}`)}
onPress={() => setSelectedLabelGroup(def.id)}> onPress={() => next(def.id)}>
<LabelGroupButton labelGroup={def.id} /> <LabelGroupButton
name={strings.name}
description={strings.description}
/>
</Button> </Button>
) )
})} })}
<Button
label={_(msg`Create report for other reasons`)}
onPress={() => next('other')}>
<LabelGroupButton
name="Other"
description="An issue not covered by another option"
/>
</Button>
{params.type === 'content' && (
<View style={[a.pt_md, a.px_md]}>
<View
style={[
a.flex_row,
a.align_center,
a.justify_between,
a.gap_md,
a.p_md,
a.pl_lg,
a.rounded_md,
t.atoms.bg_contrast_900,
]}>
<Text style={[t.atoms.text_inverted, a.italic]}>
Need to report a copyright violation?
</Text>
<Link
to={DMCA_LINK}
label={_(
msg`View details for reporting a copyright violation`,
)}
size="small"
variant="solid"
color="secondary">
<ButtonText>View details</ButtonText>
<ButtonIcon position="right" icon={SquareArrowTopRight} />
</Link>
</View>
</View>
)}
</View> </View>
</View> </View>
)} )}
@@ -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',
})
+62 -1
View File
@@ -1,5 +1,11 @@
import React from 'react' 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 {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles' import {sanitizeHandle} from '#/lib/strings/handles'
@@ -39,6 +45,33 @@ export function useConfigurableLabelGroups() {
return React.useMemo(() => getConfigurableLabelGroups(), []) 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({ export function getModerationServiceTitle({
displayName, displayName,
handle, handle,
@@ -50,3 +83,31 @@ export function getModerationServiceTitle({
? sanitizeDisplayName(displayName) ? sanitizeDisplayName(displayName)
: sanitizeHandle(handle, '@') : 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
}
+5 -1
View File
@@ -4,7 +4,7 @@ import {useLingui} from '@lingui/react'
import {useMemo} from 'react' import {useMemo} from 'react'
export type LabelGroupStrings = Record< export type LabelGroupStrings = Record<
keyof typeof LABEL_GROUPS, keyof typeof LABEL_GROUPS | 'other',
{name: string; description: string} {name: string; description: string}
> >
@@ -116,6 +116,10 @@ export function useLabelGroupStrings(): LabelGroupStrings {
msg`Helpful annotations to explain intent, such as satire or parody.`, 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.`),
},
}), }),
[_], [_],
) )
+2 -4
View File
@@ -24,8 +24,7 @@ function LabelerToggle({
preferences: UsePreferencesQueryResponse preferences: UsePreferencesQueryResponse
}) { }) {
const t = useTheme() const t = useTheme()
const {mutateAsync, variables, reset} = const {mutateAsync, variables} = useModServiceLabelGroupEnableMutation()
useModServiceLabelGroupEnableMutation()
const modservicePreferences = preferences.moderationOpts.mods.find( const modservicePreferences = preferences.moderationOpts.mods.find(
({did}) => did === labeler.creator.did, ({did}) => did === labeler.creator.did,
@@ -46,12 +45,11 @@ function LabelerToggle({
group: labelGroup, group: labelGroup,
enabled: !enabled, enabled: !enabled,
}) })
reset() // Important: clears query `variables`
} catch (e: any) { } catch (e: any) {
// TODO // TODO
console.error(e) console.error(e)
} }
}, [mutateAsync, enabled, modservicePreferences, labelGroup, reset]) }, [mutateAsync, enabled, modservicePreferences, labelGroup])
return ( return (
<Toggle.Item <Toggle.Item
name={labeler.creator.did} name={labeler.creator.did}
@@ -1,6 +1,7 @@
import React from 'react' import React from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
// import {msg} from '@lingui/macro' // import {msg} from '@lingui/macro'
import {LABEL_GROUPS} from '@atproto/api' import {LABEL_GROUPS} from '@atproto/api'
// TODO // TODO
@@ -24,7 +25,7 @@ export function PreferenceRow({
const {_} = useLingui() const {_} = useLingui()
const labelGroupStrings = useLabelGroupStrings() const labelGroupStrings = useLabelGroupStrings()
const groupInfoStrings = labelGroupStrings[labelGroup] const groupInfoStrings = labelGroupStrings[labelGroup]
const {mutateAsync, variables, reset} = useModServiceLabelGroupEnableMutation() const {mutateAsync, variables} = useModServiceLabelGroupEnableMutation()
const enabled = const enabled =
variables?.enabled ?? variables?.enabled ??
!modservicePreferences?.disabledLabelGroups?.includes(labelGroup) !modservicePreferences?.disabledLabelGroups?.includes(labelGroup)
@@ -37,12 +38,11 @@ export function PreferenceRow({
group: labelGroup, group: labelGroup,
enabled: !enabled, enabled: !enabled,
}) })
reset() // Important: clears query `variables`
} catch (e: any) { } catch (e: any) {
// TODO // TODO
console.error(e) console.error(e)
} }
}, [mutateAsync, enabled, modservicePreferences, labelGroup, reset]) }, [mutateAsync, enabled, modservicePreferences, labelGroup])
return ( return (
<View <View
@@ -65,7 +65,7 @@ export function PreferenceRow({
name="enable" name="enable"
value={enabled} value={enabled}
onChange={onToggleEnabled} onChange={onToggleEnabled}
label="Enable"> label={_(msg`Enable`)}>
<Toggle.Label>{enabled ? 'Enabled' : 'Disabled'}</Toggle.Label> <Toggle.Label>{enabled ? 'Enabled' : 'Disabled'}</Toggle.Label>
<Toggle.Switch /> <Toggle.Switch />
</Toggle.Item> </Toggle.Item>
+13 -5
View File
@@ -58,6 +58,9 @@ import {LabelInfo} from '../util/moderation/LabelInfo'
import {useProfileShadow} from 'state/cache/profile-shadow' import {useProfileShadow} from 'state/cache/profile-shadow'
import * as ModerationServiceCard from '#/components/ModerationServiceCard' import * as ModerationServiceCard from '#/components/ModerationServiceCard'
import {getModerationServiceTitle} from '#/lib/moderation' 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' import {useTheme} from '#/alf'
@@ -119,6 +122,7 @@ let ProfileHeader = ({
() => moderateProfile(profile, moderationOpts), () => moderateProfile(profile, moderationOpts),
[profile, moderationOpts], [profile, moderationOpts],
) )
const openDialog = useOpenGlobalDialog()
const invalidateProfileQuery = React.useCallback(() => { const invalidateProfileQuery = React.useCallback(() => {
queryClient.invalidateQueries({ queryClient.invalidateQueries({
@@ -280,11 +284,15 @@ let ProfileHeader = ({
const onPressReportAccount = React.useCallback(() => { const onPressReportAccount = React.useCallback(() => {
track('ProfileHeader:ReportAccountButtonClicked') track('ProfileHeader:ReportAccountButtonClicked')
openModal({ if (NEW_REPORT_DIALOG_ENABLED) {
name: 'report', openDialog(ReportDialog, {type: 'profile', did: profile.did})
did: profile.did, } else {
}) openModal({
}, [track, openModal, profile]) name: 'report',
did: profile.did,
})
}
}, [track, openModal, profile, openDialog])
const isMe = React.useMemo( const isMe = React.useMemo(
() => currentAccount?.did === profile.did, () => currentAccount?.did === profile.did,
+5 -1
View File
@@ -214,7 +214,11 @@ let PostDropdownBtn = ({
label: _(msg`Report post`), label: _(msg`Report post`),
onPress() { onPress() {
if (NEW_REPORT_DIALOG_ENABLED) { if (NEW_REPORT_DIALOG_ENABLED) {
openDialog(ReportDialog, {type: 'post', uri: postUri, cid: postCid}) openDialog(ReportDialog, {
type: 'content',
uri: postUri,
cid: postCid,
})
} else { } else {
openModal({ openModal({
name: 'report', name: 'report',