Hook up data

This commit is contained in:
Eric Bailey
2024-02-14 19:05:37 -06:00
parent cc635ae949
commit 7313e70ca7
12 changed files with 751 additions and 391 deletions
+152
View File
@@ -0,0 +1,152 @@
import React from 'react'
import {View} from 'react-native'
import {AppBskyModerationDefs} from '@atproto/api'
import {LabelGroupDefinition} from '@atproto/api'
import {Trans} from '@lingui/macro'
import {useTheme, atoms as a} from '#/alf'
import {Text} from '#/components/Typography'
import * as Toggle from '#/components/forms/Toggle'
import {useLabelGroupStrings} from '#/lib/moderation/useLabelGroupStrings'
import * as Dialog from '#/components/Dialog'
import * as ModerationServiceCard from '#/components/ModerationServiceCard'
import {getModerationServiceTitle} from '#/lib/moderation'
import {UsePreferencesQueryResponse} from '#/state/queries/preferences'
import {useModServiceLabelGroupEnableMutation} from '#/state/queries/modservice'
function LabelerToggle({
labelGroup,
labeler,
preferences,
}: {
labelGroup: LabelGroupDefinition['id']
labeler: AppBskyModerationDefs.ModServiceViewDetailed
preferences: UsePreferencesQueryResponse
}) {
const t = useTheme()
const {mutateAsync, variables, reset} =
useModServiceLabelGroupEnableMutation()
const modservicePreferences = preferences.moderationOpts.mods.find(
({did}) => did === labeler.creator.did,
)
const enabled =
variables?.enabled ??
!modservicePreferences?.disabledLabelGroups?.includes(labelGroup)
const title = getModerationServiceTitle({
displayName: labeler.creator.displayName,
handle: labeler.creator.handle,
})
const onToggleEnabled = React.useCallback(async () => {
try {
await mutateAsync({
// @ts-ignore TODO
did: modservicePreferences?.did,
group: labelGroup,
enabled: !enabled,
})
reset() // Important: clears query `variables`
} catch (e: any) {
// TODO
console.error(e)
}
}, [mutateAsync, enabled, modservicePreferences, labelGroup, reset])
return (
<Toggle.Item
name={labeler.creator.did}
label={title}
key={labeler.creator.did}
value={enabled}
onChange={onToggleEnabled}>
{ctx => (
<ModerationServiceCard.Card.Outer
style={[
...(ctx.focused || ctx.hovered ? [t.atoms.bg_contrast_50] : []),
ctx.selected && {
backgroundColor: t.palette.primary_50,
borderColor: t.palette.primary_100,
},
]}>
<ModerationServiceCard.Card.Avatar avatar={labeler.creator.avatar} />
<ModerationServiceCard.Card.Content
title={title}
description={labeler.description}
handle={labeler.creator.handle}
/>
</ModerationServiceCard.Card.Outer>
)}
</Toggle.Item>
)
}
export type SettingsDialogProps = {
labelGroup: LabelGroupDefinition['id']
modservices: AppBskyModerationDefs.ModServiceViewDetailed[]
}
export function SettingsDialog({
labelGroup,
modservices,
preferences,
}: SettingsDialogProps & {
preferences: UsePreferencesQueryResponse
}) {
const t = useTheme()
const labelGroupStrings = useLabelGroupStrings()
const groupInfoStrings = labelGroupStrings[labelGroup]
return (
<Dialog.ScrollableInner label="Configure moderation service settings">
<Dialog.Close />
<Text style={[a.text_xl, a.font_bold, a.pb_xl]}>
<Trans>Configure enabled labelers</Trans>
</Text>
<View
style={[
a.w_full,
a.flex_row,
a.align_center,
a.justify_between,
a.gap_lg,
a.p_md,
a.rounded_md,
a.mb_xl,
a.border,
t.atoms.border_contrast_low,
]}>
<View style={[a.flex_1, a.gap_xs]}>
<Text style={[a.text_md, a.font_bold]}>{groupInfoStrings.name}</Text>
<Text style={[a.leading_tight, {maxWidth: 400}]}>
{groupInfoStrings.description}
</Text>
</View>
</View>
<Text
style={[
a.leading_snug,
a.font_bold,
t.atoms.text_contrast_medium,
a.pb_lg,
]}>
<Trans>Select which labelers to use for this type of content:</Trans>
</Text>
<View style={[a.w_full, a.gap_md]}>
{modservices.map(modservice => {
return (
<LabelerToggle
key={modservice.creator.did}
labelGroup={labelGroup}
labeler={modservice}
preferences={preferences}
/>
)
})}
</View>
</Dialog.ScrollableInner>
)
}
+276 -211
View File
@@ -1,14 +1,10 @@
import React from 'react'
import {View} from 'react-native'
import {useFocusEffect} from '@react-navigation/native'
import {ComAtprotoLabelDefs} from '@atproto/api'
import {ComAtprotoLabelDefs, LabelPreference} from '@atproto/api'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {
LABEL_GROUPS,
LabelGroupDefinition,
DEFAULT_LABEL_GROUP_SETTINGS,
} from '@atproto/api'
import {LabelGroupDefinition, AppBskyModerationDefs} from '@atproto/api'
import {NativeStackScreenProps, CommonNavigatorParams} from '#/lib/routes/types'
import {CenteredView} from '#/view/com/util/Views'
@@ -22,12 +18,18 @@ import {
} from '#/state/queries/profile'
import {ScrollView} from '#/view/com/util/Views'
import {
UsePreferencesQueryResponse,
usePreferencesQuery,
useSetContentLabelMutation,
} from '#/state/queries/preferences'
import {useModServicesInfoQuery} from '#/state/queries/modservice'
import {useTheme, atoms as a, useBreakpoints} from '#/alf'
import {Divider} from '#/components/Divider'
import {CircleBanSign_Stroke2_Corner0_Rounded as CircleBanSign} from '#/components/icons/CircleBanSign'
import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group'
import {Person_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person'
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import {Text} from '#/components/Typography'
import * as Toggle from '#/components/forms/Toggle'
import * as ToggleButton from '#/components/forms/ToggleButton'
@@ -36,98 +38,71 @@ import {Loader} from '#/components/Loader'
import {useLabelGroupStrings} from '#/lib/moderation/useLabelGroupStrings'
import * as Dialog from '#/components/Dialog'
import {Button} from '#/components/Button'
import {
getLabelGroupsFromLabels,
getModerationServiceTitle,
useConfigurableLabelGroups,
} from '#/lib/moderation'
function ModSettingsToggleItem({name}: {name: string}) {
import {
SettingsDialog,
SettingsDialogProps,
} from '#/screens/Moderation/SettingsDialog'
export function ModerationScreen(
_props: NativeStackScreenProps<CommonNavigatorParams, 'Moderation'>,
) {
const t = useTheme()
const ctx = Toggle.useItemContext()
return (
<View
style={[
a.w_full,
a.flex_row,
a.justify_between,
a.align_center,
a.p_md,
a.rounded_sm,
t.atoms.bg_contrast_25,
{
backgroundColor: ctx.selected ? t.palette.primary_25 : undefined,
},
]}>
<Text>{name}</Text>
{ctx.selected && <Check />}
const {
isLoading: isPreferencesLoading,
// error: preferencesError,
data: preferences,
} = usePreferencesQuery()
return isPreferencesLoading ? (
<View style={[a.w_full, a.align_center]}>
<Loader size="xl" fill={t.atoms.text.color} />
</View>
)
) : preferences ? (
<ModerationScreenIntermediate preferences={preferences} />
) : // TODO
null
}
function ModSettingsDialog({
name,
onComplete,
function ModerationScreenIntermediate({
preferences,
}: {
name: string
onComplete: () => void
preferences: UsePreferencesQueryResponse
}) {
const t = useTheme()
const labelGroupStrings = useLabelGroupStrings()
const [selectedServices, setSelectedServices] = React.useState<string[]>([
'bluesky',
])
const {
isLoading: isModServicesLoading,
data: modservices,
// error: modservicesError,
} = useModServicesInfoQuery({
dids: preferences.moderationOpts.mods.map(m => m.did),
})
const save = React.useCallback(() => {
onComplete()
}, [onComplete])
return (
<Dialog.Inner label="Configure moderation service settings">
<Text style={[a.text_xl, a.font_bold, a.pb_sm]}>
Configure moderation services
</Text>
<Text
style={[
a.text_md,
a.leading_snug,
t.atoms.text_contrast_high,
a.pb_lg,
]}>
Select which moderation services' labels you'd like to use to filter
content matching {labelGroupStrings[name].name}.
</Text>
<Toggle.Group
values={selectedServices}
onChange={setSelectedServices}
label="Select one or more">
<View style={[a.w_full, a.gap_md]}>
<Toggle.Item name="bluesky" label="Bluesky">
<ModSettingsToggleItem name="Bluesky" />
</Toggle.Item>
<Toggle.Item name="contraption" label="Contraption">
<ModSettingsToggleItem name="Contraption" />
</Toggle.Item>
<Toggle.Item name="safety" label="Safety Corp">
<ModSettingsToggleItem name="Safety Corp" />
</Toggle.Item>
</View>
</Toggle.Group>
<View style={[a.flex_row, a.justify_end, a.pt_lg]}>
<Button
size="large"
variant="solid"
color="primary"
label="Save"
onPress={save}>
{selectedServices.length ? 'Save' : 'Ignore all'}
</Button>
</View>
</Dialog.Inner>
)
return isModServicesLoading ? (
<View style={[a.w_full, a.align_center]}>
<Loader size="xl" fill={t.atoms.text.color} />
</View>
) : modservices ? (
<ModerationScreenInner
preferences={preferences}
modservices={modservices}
/>
) : // TODO
null
}
export function ModerationScreen({}: NativeStackScreenProps<
CommonNavigatorParams,
'Moderation'
>) {
export function ModerationScreenInner({
preferences,
modservices,
}: {
preferences: UsePreferencesQueryResponse
modservices: AppBskyModerationDefs.ModServiceViewDetailed[]
}) {
const t = useTheme()
const {_} = useLingui()
const setMinimalShellMode = useSetMinimalShellMode()
@@ -135,8 +110,13 @@ export function ModerationScreen({}: NativeStackScreenProps<
const {gtMobile, gtTablet} = useBreakpoints()
const labelGroupStrings = useLabelGroupStrings()
const modSettingsDialogControl = Dialog.useDialogControl()
const [modSettingsDialogLabelGroup, setModSettingsDialogLabelGroup] =
React.useState<string>(() => Object.keys(LABEL_GROUPS)[0])
const [settingsDialogProps, setSettingsDialogProps] =
React.useState<SettingsDialogProps>({
// @ts-ignore
labelGroup: '',
modservices: [],
})
useFocusEffect(
React.useCallback(() => {
@@ -145,20 +125,40 @@ export function ModerationScreen({}: NativeStackScreenProps<
}, [screen, setMinimalShellMode]),
)
const groups = React.useMemo<
[keyof typeof LABEL_GROUPS, LabelGroupDefinition][]
const groups = useConfigurableLabelGroups()
const didToModServiceMap = React.useMemo<
Record<string, AppBskyModerationDefs.ModServiceViewDetailed>
>(() => {
return Object.entries(LABEL_GROUPS).filter(([, def]) => def.configurable)
}, [])
const labelOptions = {
hide: _(msg`Hide`),
warn: _(msg`Warn`),
show: _(msg`Show`),
}
return modservices.reduce((acc, modservice) => {
return {
...acc,
[modservice.creator.did]: modservice,
}
}, {})
}, [modservices])
const labelGroupToModServiceMap = React.useMemo(() => {
const groups: Partial<Record<LabelGroupDefinition['id'], string[]>> = {}
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.creator.did)
}
}
return groups
}, [modservices])
const openModSettingsDialog = React.useCallback(
({name}: {name: string}) => {
setModSettingsDialogLabelGroup(name)
({labelGroup, modservices}: Omit<SettingsDialogProps, 'onComplete'>) => {
setSettingsDialogProps({
labelGroup,
modservices,
})
modSettingsDialogControl.open()
},
[modSettingsDialogControl],
@@ -175,11 +175,7 @@ export function ModerationScreen({}: NativeStackScreenProps<
testID="moderationScreen">
<Dialog.Outer control={modSettingsDialogControl}>
<Dialog.Handle />
<ModSettingsDialog
name={modSettingsDialogLabelGroup}
onComplete={() => modSettingsDialogControl.close()}
/>
<SettingsDialog {...settingsDialogProps} preferences={preferences} />
</Dialog.Outer>
<ViewHeader title={_(msg`Moderation`)} showOnDesktop />
@@ -255,114 +251,21 @@ export function ModerationScreen({}: NativeStackScreenProps<
<Trans>Content filtering settings</Trans>
</Text>
{groups.map(([name, def], i) => {
const groupStrings = labelGroupStrings[name]
{groups.map((def, i) => {
const groupStrings = labelGroupStrings[def.id]
const modDids = labelGroupToModServiceMap[def.id] || []
const mods = modDids.map(did => didToModServiceMap[did])
return (
<React.Fragment key={def.id}>
{i !== 0 && <Divider />}
<View style={[a.pb_md]}>
<View
style={[
a.py_md,
a.flex_row,
a.justify_between,
a.gap_sm,
a.align_center,
]}>
<View style={[a.gap_xs, {width: '50%'}]}>
<Text
style={[
a.text_md,
a.font_bold,
t.atoms.text_contrast_medium,
]}>
{groupStrings.name}
</Text>
<Text style={[a.leading_tight, {maxWidth: 400}]}>
{groupStrings.description}
</Text>
</View>
<View>
<ToggleButton.Group
label={_(
msg`Configure content filtering setting for category: ${groupStrings.name.toLowerCase()}`,
)}
values={[DEFAULT_LABEL_GROUP_SETTINGS[name]]}
onChange={() => {}}>
<ToggleButton.Button
name="hide"
label={labelOptions.hide}>
{labelOptions.hide}
</ToggleButton.Button>
<ToggleButton.Button
name="warn"
label={labelOptions.warn}>
{labelOptions.warn}
</ToggleButton.Button>
<ToggleButton.Button
name="ignore"
label={labelOptions.show}>
{labelOptions.show}
</ToggleButton.Button>
</ToggleButton.Group>
</View>
</View>
<View style={[a.flex_row, a.align_start]}>
<Button
label={_(
msg`Configure moderation services for category: ${groupStrings.name.toLowerCase()}`,
)}
onPress={() =>
openModSettingsDialog({
name,
})
}>
<View style={[a.flex_row, a.align_start, a.gap_xs]}>
<View
style={[
a.py_sm,
a.px_md,
a.rounded_full,
t.atoms.bg_contrast_800,
]}>
<Text
style={[
a.text_xs,
a.font_bold,
t.atoms.text_inverted,
]}>
Bluesky
</Text>
</View>
<View
style={[
a.py_sm,
a.px_md,
a.rounded_full,
t.atoms.bg_contrast_25,
]}>
<Text style={[a.text_xs, a.font_bold]}>
Contraption
</Text>
</View>
<View
style={[
a.py_sm,
a.px_md,
a.rounded_full,
t.atoms.bg_contrast_25,
]}>
<Text style={[a.text_xs, a.font_bold]}>
Safety Corp
</Text>
</View>
</View>
</Button>
</View>
</View>
<LabelGroup
name={groupStrings.name}
description={groupStrings.description}
labelers={mods}
openModSettingsDialog={openModSettingsDialog}
preferences={preferences}
labelGroup={def.id}
/>
</React.Fragment>
)
})}
@@ -381,6 +284,168 @@ export function ModerationScreen({}: NativeStackScreenProps<
)
}
function LabelGroup({
labelGroup,
name,
description,
labelers: mods,
preferences,
openModSettingsDialog,
}: {
labelGroup: LabelGroupDefinition['id']
name: string
description: string
labelers: AppBskyModerationDefs.ModServiceViewDetailed[]
preferences: UsePreferencesQueryResponse
openModSettingsDialog: (props: SettingsDialogProps) => void
}) {
const t = useTheme()
const {_} = useLingui()
const {mutateAsync: setContentLabelPref, variables: optimisticContentLabel} =
useSetContentLabelMutation()
const onChangeVisibility = React.useCallback(
async (values: string[]) => {
try {
await setContentLabelPref({
labelGroup,
visibility: values[0] as LabelPreference,
})
} catch (e) {
console.error(e)
}
},
[labelGroup, setContentLabelPref],
)
const value =
optimisticContentLabel?.visibility ??
preferences.moderationOpts.labelGroups[labelGroup]
const labelOptions = {
hide: _(msg`Hide`),
warn: _(msg`Warn`),
show: _(msg`Show`),
}
return (
<View style={[]}>
<View
style={[
a.py_md,
a.flex_row,
a.justify_between,
a.gap_sm,
a.align_center,
]}>
<View style={[a.gap_xs, {width: '50%'}]}>
<Text style={[a.text_md, a.font_bold, t.atoms.text_contrast_medium]}>
{name}
</Text>
<Text style={[a.leading_tight, {maxWidth: 400}]}>{description}</Text>
</View>
<View>
<ToggleButton.Group
label={_(
msg`Configure content filtering setting for category: ${name.toLowerCase()}`,
)}
values={[value]}
onChange={onChangeVisibility}>
<ToggleButton.Button name="hide" label={labelOptions.hide}>
{labelOptions.hide}
</ToggleButton.Button>
<ToggleButton.Button name="warn" label={labelOptions.warn}>
{labelOptions.warn}
</ToggleButton.Button>
<ToggleButton.Button name="ignore" label={labelOptions.show}>
{labelOptions.show}
</ToggleButton.Button>
</ToggleButton.Group>
</View>
</View>
{!!mods.length && (
<View style={[a.flex_row, a.align_start, a.pb_md]}>
<Button
label={_(
msg`Configure moderation services for category: ${name.toLowerCase()}`,
)}
onPress={() =>
openModSettingsDialog({
labelGroup,
modservices: mods,
})
}
style={[a.flex_1]}>
{ctx => (
<View
style={[
a.w_full,
a.flex_row,
a.align_center,
a.gap_xs,
a.p_sm,
a.border,
a.rounded_sm,
t.atoms.border_contrast_low,
(ctx.hovered || ctx.focused) && t.atoms.bg_contrast_25,
]}>
{mods.map(mod => {
const modservicePreferences =
preferences.moderationOpts.mods.find(
({did}) => did === mod.creator.did,
)
const enabled =
!modservicePreferences?.disabledLabelGroups?.includes(
labelGroup,
)
return (
<View
key={mod.creator.did}
style={[
a.py_xs,
a.px_sm,
a.rounded_sm,
a.border,
t.atoms.border_contrast_low,
enabled && t.atoms.bg_contrast_800,
]}>
<Text
style={[
a.text_xs,
a.font_bold,
t.atoms.text_contrast_high,
enabled && t.atoms.text_inverted,
]}>
{getModerationServiceTitle({
displayName: mod.creator.displayName,
handle: mod.creator.handle,
})}
</Text>
</View>
)
})}
<Text
style={[
a.text_xs,
a.font_bold,
a.pl_sm,
a.italic,
t.atoms.text_contrast_low,
]}>
Configure
</Text>
</View>
)}
</Button>
</View>
)}
</View>
)
}
function PwiOptOut() {
const t = useTheme()
const {_} = useLingui()