From 19c0ae80eb62f61472457c1819504165ce4552d1 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Mon, 4 Mar 2024 18:14:02 -0800 Subject: [PATCH] Implement preference controls on labelers --- .../ModerationLabelPref/PreferenceButton.tsx | 133 +++++++++++++ .../PreferenceButton.web.tsx | 102 ++++++++++ src/components/ModerationLabelPref/index.tsx | 52 +++-- .../moderation/useLabelBehaviorDescription.ts | 48 ++++- .../Profile/Header/ProfileHeaderLabeler.tsx | 2 +- src/screens/Profile/Sections/Labels.tsx | 183 ++++++++++++------ src/state/queries/preferences/index.ts | 6 +- src/view/screens/Profile.tsx | 11 +- 8 files changed, 440 insertions(+), 97 deletions(-) create mode 100644 src/components/ModerationLabelPref/PreferenceButton.tsx create mode 100644 src/components/ModerationLabelPref/PreferenceButton.web.tsx diff --git a/src/components/ModerationLabelPref/PreferenceButton.tsx b/src/components/ModerationLabelPref/PreferenceButton.tsx new file mode 100644 index 0000000000..199b61ed7c --- /dev/null +++ b/src/components/ModerationLabelPref/PreferenceButton.tsx @@ -0,0 +1,133 @@ +import React from 'react' +import {Pressable} from 'react-native' +import {useLingui} from '@lingui/react' +import {msg, Trans} from '@lingui/macro' +import {InterprettedLabelValueDefinition, LabelPreference} from '@atproto/api' + +import { + useLabelBehaviorDescription, + useLabelLongBehaviorDescription, +} from '#/lib/moderation/useLabelBehaviorDescription' + +import {useTheme, atoms as a} from '#/alf' +import * as Dialog from '#/components/Dialog' +import {Text} from '#/components/Typography' +import {Button, ButtonText, ButtonIcon} from '#/components/Button' +import {ArrowTriangleBottom_Stroke2_Corner1_Rounded as ArrowTriangleBottom} from '../icons/ArrowTriangle' +import {Check_Stroke2_Corner0_Rounded as Check} from '../icons/Check' + +export function PreferenceButton({ + name, + pref, + labelValueDefinition, + onSelectPref, +}: { + name: string + pref: LabelPreference + labelValueDefinition: InterprettedLabelValueDefinition + onSelectPref: (pref: LabelPreference) => void +}) { + const {_} = useLingui() + const t = useTheme() + const control = Dialog.useDialogControl() + + const settingDesc = useLabelBehaviorDescription(labelValueDefinition, pref) + const hideLabel = useLabelLongBehaviorDescription( + labelValueDefinition, + 'hide', + ) + const warnLabel = useLabelLongBehaviorDescription( + labelValueDefinition, + 'warn', + ) + const ignoreLabel = useLabelLongBehaviorDescription( + labelValueDefinition, + 'ignore', + ) + const canWarn = !( + labelValueDefinition.blurs === 'none' && + labelValueDefinition.severity === 'none' + ) + + return ( + <> + control.open()} + accessibilityLabel={settingDesc} + accessibilityHint="" + style={[ + a.flex_row, + a.align_center, + a.justify_end, + a.gap_xs, + a.py_xs, + a.rounded_2xs, + ]}> + + {settingDesc} + + + + + + + + + + {name} + + + Choose how this label should be handled. + + + + + {canWarn && ( + + )} + + + + + + ) +} diff --git a/src/components/ModerationLabelPref/PreferenceButton.web.tsx b/src/components/ModerationLabelPref/PreferenceButton.web.tsx new file mode 100644 index 0000000000..8275686241 --- /dev/null +++ b/src/components/ModerationLabelPref/PreferenceButton.web.tsx @@ -0,0 +1,102 @@ +import React from 'react' +import {View} from 'react-native' +import {useLingui} from '@lingui/react' +import {msg} from '@lingui/macro' +import {InterprettedLabelValueDefinition, LabelPreference} from '@atproto/api' + +import {useLabelBehaviorDescription} from '#/lib/moderation/useLabelBehaviorDescription' +import { + NativeDropdown, + DropdownItem, +} from '#/view/com/util/forms/NativeDropdown' + +import {useTheme, atoms as a} from '#/alf' +import {Text} from '#/components/Typography' +import {ArrowTriangleBottom_Stroke2_Corner1_Rounded as ArrowTriangleBottom} from '../icons/ArrowTriangle' +import {useInteractionState} from '#/components/hooks/useInteractionState' + +const CHECK_ICON: DropdownItem['icon'] = { + web: ['fas', 'check'], + ios: {name: 'trash'}, //doesnt matter + android: '', +} + +export function PreferenceButton({ + pref, + labelValueDefinition, + onSelectPref, +}: { + pref: LabelPreference + labelValueDefinition: InterprettedLabelValueDefinition + onSelectPref: (pref: LabelPreference) => void +}) { + const {_} = useLingui() + const t = useTheme() + + const { + state: hovered, + onIn: onHoverIn, + onOut: onHoverOut, + } = useInteractionState() + + const settingDesc = useLabelBehaviorDescription(labelValueDefinition, pref) + const hideLabel = useLabelBehaviorDescription(labelValueDefinition, 'hide') + const warnLabel = useLabelBehaviorDescription(labelValueDefinition, 'warn') + const ignoreLabel = useLabelBehaviorDescription( + labelValueDefinition, + 'ignore', + ) + const canWarn = !( + labelValueDefinition.blurs === 'none' && + labelValueDefinition.severity === 'none' + ) + + const dropdownItems: DropdownItem[] = [] + dropdownItems.push({ + icon: pref === 'hide' ? CHECK_ICON : undefined, + label: hideLabel, + onPress: () => onSelectPref('hide'), + }) + if (canWarn) { + dropdownItems.push({ + icon: pref === 'warn' ? CHECK_ICON : undefined, + label: warnLabel, + onPress: () => onSelectPref('warn'), + }) + } + dropdownItems.push({ + icon: pref === 'ignore' ? CHECK_ICON : undefined, + label: ignoreLabel, + onPress: () => onSelectPref('ignore'), + }) + + return ( + + + + {settingDesc} + + + + + ) +} diff --git a/src/components/ModerationLabelPref/index.tsx b/src/components/ModerationLabelPref/index.tsx index af8c97c76a..f80e6d8387 100644 --- a/src/components/ModerationLabelPref/index.tsx +++ b/src/components/ModerationLabelPref/index.tsx @@ -1,24 +1,32 @@ import React from 'react' import {View} from 'react-native' -import {InterprettedLabelValueDefinition} from '@atproto/api' +import {InterprettedLabelValueDefinition, LabelPreference} from '@atproto/api' import {useLabelStrings} from '#/lib/moderation/useLabelStrings' -import {useLabelBehaviorDescription} from '#/lib/moderation/useLabelBehaviorDescription' +import { + usePreferencesQuery, + usePreferencesSetContentLabelMutation, +} from '#/state/queries/preferences' import {useTheme, atoms as a} from '#/alf' import {Text} from '#/components/Typography' -import {Button, ButtonText, ButtonIcon} from '#/components/Button' -import {ArrowTriangleBottom_Stroke2_Corner1_Rounded as ArrowTriangleBottom} from '../icons/ArrowTriangle' +import {PreferenceButton} from './PreferenceButton' export function ModerationLabelPref({ labelValueDefinition, + labelerDid, disabled, }: { labelValueDefinition: InterprettedLabelValueDefinition + labelerDid: string | undefined disabled?: boolean }) { const t = useTheme() const allLabelStrings = useLabelStrings() + const {data: preferences} = usePreferencesQuery() + const {mutate, variables} = usePreferencesSetContentLabelMutation() + + const {identifier} = labelValueDefinition const labelStrings = labelValueDefinition.locales[0] // TODO look up locale ? labelValueDefinition.locales[0] : labelValueDefinition.identifier in allLabelStrings @@ -27,9 +35,16 @@ export function ModerationLabelPref({ name: labelValueDefinition.identifier, description: `Labeled "${labelValueDefinition.identifier}"`, } - const settingDesc = useLabelBehaviorDescription(labelValueDefinition, 'hide') - // TODO add onChange behavior when mod prefs are updated + const savedPref = labelerDid + ? preferences?.moderationPrefs.mods.find(m => m.did === labelerDid)?.labels[ + identifier + ] + : preferences?.moderationPrefs.labels[identifier] + const pref = variables?.visibility ?? savedPref ?? 'warn' + + const onSelectPref = (newPref: LabelPreference) => + mutate({label: identifier, visibility: newPref, labelerDid}) return ( @@ -39,21 +54,16 @@ export function ModerationLabelPref({ {labelStrings.description} - {!disabled && ( - - - {settingDesc} - - - - )} + + {!disabled && ( + + )} + ) } diff --git a/src/lib/moderation/useLabelBehaviorDescription.ts b/src/lib/moderation/useLabelBehaviorDescription.ts index 43110d72f7..e4624f3624 100644 --- a/src/lib/moderation/useLabelBehaviorDescription.ts +++ b/src/lib/moderation/useLabelBehaviorDescription.ts @@ -12,22 +12,58 @@ export function useLabelBehaviorDescription( } if (labelValueDef.blurs === 'content') { if (pref === 'hide') { - return _(msg`Hide content`) + return _(msg`Hide`) } - return _(msg`Warn content`) + return _(msg`Warn`) } else if (labelValueDef.blurs === 'media') { if (pref === 'hide') { - return _(msg`Hide images`) + return _(msg`Hide`) } - return _(msg`Warn images`) + return _(msg`Blur images`) } else if (labelValueDef.severity === 'alert') { if (pref === 'hide') { - return _(msg`Filter from feeds`) + return _(msg`Hide`) } return _(msg`Show warning`) } else if (labelValueDef.severity === 'inform') { if (pref === 'hide') { - return _(msg`Filter from feeds`) + return _(msg`Hide`) + } + return _(msg`Show badge`) + } else { + if (pref === 'hide') { + return _(msg`Hide`) + } + return _(msg`Disabled`) + } +} + +export function useLabelLongBehaviorDescription( + labelValueDef: InterprettedLabelValueDefinition, + pref: LabelPreference, +) { + const {_} = useLingui() + if (pref === 'ignore') { + return _(msg`Disabled`) + } + if (labelValueDef.blurs === 'content') { + if (pref === 'hide') { + return _(msg`Warn content and filter from feeds`) + } + return _(msg`Warn content`) + } else if (labelValueDef.blurs === 'media') { + if (pref === 'hide') { + return _(msg`Blur images and filter from feeds`) + } + return _(msg`Blur images`) + } else if (labelValueDef.severity === 'alert') { + if (pref === 'hide') { + return _(msg`Show warning and filter from feeds`) + } + return _(msg`Show warning`) + } else if (labelValueDef.severity === 'inform') { + if (pref === 'hide') { + return _(msg`Show badge and filter from feeds`) } return _(msg`Show badge`) } else { diff --git a/src/screens/Profile/Header/ProfileHeaderLabeler.tsx b/src/screens/Profile/Header/ProfileHeaderLabeler.tsx index dbaeae315c..5f4c2f8228 100644 --- a/src/screens/Profile/Header/ProfileHeaderLabeler.tsx +++ b/src/screens/Profile/Header/ProfileHeaderLabeler.tsx @@ -137,7 +137,7 @@ let ProfileHeaderLabeler = ({ isPlaceholderProfile={isPlaceholderProfile}> {isMe ? (