diff --git a/assets/icons/settingsGear2_stroke2_corner0_rounded.svg b/assets/icons/settingsGear2_stroke2_corner0_rounded.svg
new file mode 100644
index 0000000000..de8a579194
--- /dev/null
+++ b/assets/icons/settingsGear2_stroke2_corner0_rounded.svg
@@ -0,0 +1 @@
+
diff --git a/src/components/forms/Toggle.tsx b/src/components/forms/Toggle.tsx
index 168639d6f8..65c2c1ce4f 100644
--- a/src/components/forms/Toggle.tsx
+++ b/src/components/forms/Toggle.tsx
@@ -49,12 +49,12 @@ const GroupContext = React.createContext<{
setFieldValue: () => {},
})
-export type GroupProps = React.PropsWithChildren<{
+export type GroupProps = React.PropsWithChildren<{
type?: 'radio' | 'checkbox'
- values: string[]
+ values: T[]
maxSelections?: number
disabled?: boolean
- onChange: (value: string[]) => void
+ onChange: (value: T[]) => void
label: string
}>
@@ -73,7 +73,7 @@ export function useItemContext() {
return React.useContext(ItemContext)
}
-export function Group({
+export function Group({
children,
values: providedValues,
onChange,
@@ -81,13 +81,13 @@ export function Group({
type = 'checkbox',
maxSelections,
label,
-}: GroupProps) {
+}: GroupProps) {
const groupRole = type === 'radio' ? 'radiogroup' : undefined
const values = type === 'radio' ? providedValues.slice(0, 1) : providedValues
const [maxReached, setMaxReached] = React.useState(false)
const setFieldValue = React.useCallback<
- (props: {name: string; value: boolean}) => void
+ (props: {name: ValuesType; value: boolean}) => void
>(
({name, value}) => {
if (type === 'checkbox') {
diff --git a/src/components/icons/Filter.tsx b/src/components/icons/Filter.tsx
new file mode 100644
index 0000000000..02ac1c71b4
--- /dev/null
+++ b/src/components/icons/Filter.tsx
@@ -0,0 +1,5 @@
+import {createSinglePathSVG} from './TEMPLATE'
+
+export const Filter_Stroke2_Corner0_Rounded = createSinglePathSVG({
+ path: 'M3 4a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v4a1 1 0 0 1-.293.707L15 14.414V20a1 1 0 0 1-.758.97l-4 1A1 1 0 0 1 9 21v-6.586L3.293 8.707A1 1 0 0 1 3 8V4Zm2 1v2.586l5.707 5.707A1 1 0 0 1 11 14v5.72l2-.5V14a1 1 0 0 1 .293-.707L19 7.586V5H5Z',
+})
diff --git a/src/components/icons/Gear.tsx b/src/components/icons/Gear.tsx
new file mode 100644
index 0000000000..980b7413bd
--- /dev/null
+++ b/src/components/icons/Gear.tsx
@@ -0,0 +1,5 @@
+import {createSinglePathSVG} from './TEMPLATE'
+
+export const SettingsGear2_Stroke2_Corner0_Rounded = createSinglePathSVG({
+ path: 'M11.1 2a1 1 0 0 0-.832.445L8.851 4.57 6.6 4.05a1 1 0 0 0-.932.268l-1.35 1.35a1 1 0 0 0-.267.932l.52 2.251-2.126 1.417A1 1 0 0 0 2 11.1v1.8a1 1 0 0 0 .445.832l2.125 1.417-.52 2.251a1 1 0 0 0 .268.932l1.35 1.35a1 1 0 0 0 .932.267l2.251-.52 1.417 2.126A1 1 0 0 0 11.1 22h1.8a1 1 0 0 0 .832-.445l1.417-2.125 2.251.52a1 1 0 0 0 .932-.268l1.35-1.35a1 1 0 0 0 .267-.932l-.52-2.251 2.126-1.417A1 1 0 0 0 22 12.9v-1.8a1 1 0 0 0-.445-.832L19.43 8.851l.52-2.251a1 1 0 0 0-.268-.932l-1.35-1.35a1 1 0 0 0-.932-.267l-2.251.52-1.417-2.126A1 1 0 0 0 12.9 2h-1.8Zm-.968 4.255L11.635 4h.73l1.503 2.255a1 1 0 0 0 1.057.42l2.385-.551.566.566-.55 2.385a1 1 0 0 0 .42 1.057L20 11.635v.73l-2.255 1.503a1 1 0 0 0-.42 1.057l.551 2.385-.566.566-2.385-.55a1 1 0 0 0-1.057.42L12.365 20h-.73l-1.503-2.255a1 1 0 0 0-1.057-.42l-2.385.551-.566-.566.55-2.385a1 1 0 0 0-.42-1.057L4 12.365v-.73l2.255-1.503a1 1 0 0 0 .42-1.057L6.123 6.69l.566-.566 2.385.55a1 1 0 0 0 1.057-.42ZM8 12a4 4 0 1 1 8 0 4 4 0 0 1-8 0Zm4-2a2 2 0 1 0 0 4 2 2 0 0 0 0-4Z',
+})
diff --git a/src/components/moderation/ModerationLabelPref.tsx b/src/components/moderation/ModerationLabelPref.tsx
index 035635d551..a1ecc1a753 100644
--- a/src/components/moderation/ModerationLabelPref.tsx
+++ b/src/components/moderation/ModerationLabelPref.tsx
@@ -1,5 +1,5 @@
import React from 'react'
-import {Pressable, View} from 'react-native'
+import {View} from 'react-native'
import {InterpretedLabelValueDefinition, LabelPreference} from '@atproto/api'
import {useLingui} from '@lingui/react'
import {msg, Trans} from '@lingui/macro'
@@ -19,10 +19,11 @@ import {useTheme, atoms as a} from '#/alf'
import {Text} from '#/components/Typography'
import {InlineLink} from '#/components/Link'
import * as Dialog from '#/components/Dialog'
-import {Button, ButtonText, ButtonIcon} from '#/components/Button'
-import {ArrowTriangleBottom_Stroke2_Corner1_Rounded as ArrowTriangleBottom} from '../icons/ArrowTriangle'
+import {Button} from '#/components/Button'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '../icons/CircleInfo'
-import {Check_Stroke2_Corner0_Rounded as Check} from '../icons/Check'
+import {SettingsGear2_Stroke2_Corner0_Rounded as Gear} from '../icons/Gear'
+import * as Toggle from '#/components/forms/Toggle'
+import {Divider} from '#/components/Divider'
export function ModerationLabelPref({
labelValueDefinition,
@@ -51,6 +52,7 @@ export function ModerationLabelPref({
savedPref ??
labelValueDefinition.defaultSetting ??
'warn'
+ const [selected, setSelected] = React.useState([pref])
const settingDesc = useLabelBehaviorDescription(labelValueDefinition, pref)
const hideLabel = useLabelLongBehaviorDescription(
@@ -81,62 +83,85 @@ export function ModerationLabelPref({
adultOnly && !preferences?.moderationPrefs.adultContentEnabled
const cantConfigure = isGlobalLabel || adultDisabled
- const onSelectPref = (newPref: LabelPreference) =>
- mutate({label: identifier, visibility: newPref, labelerDid})
+ const onSettingChange = React.useCallback(
+ (newPrefs: LabelPreference[]) => {
+ setSelected(newPrefs)
+ mutate({label: identifier, visibility: newPrefs[0], labelerDid})
+ },
+ [mutate, labelerDid, identifier],
+ )
+
+ const settings = [
+ {
+ pref: 'hide',
+ label: hideLabel,
+ },
+ canWarn && {
+ pref: 'warn',
+ label: warnLabel,
+ },
+ {
+ pref: 'ignore',
+ label: ignoreLabel,
+ },
+ ].filter(Boolean) as {
+ pref: LabelPreference
+ label: string
+ }[]
return (
<>
- control.open()}
- accessibilityLabel={settingDesc}
- accessibilityHint=""
- style={[
- a.flex_row,
- a.justify_between,
- a.gap_sm,
- a.align_center,
- t.atoms.bg_contrast_25,
- a.px_lg,
- a.py_lg,
- a.rounded_sm,
- ]}>
- {labelStrings.name}
-
-
- {labelStrings.description}
-
- {!disabled && (
+ label={settingDesc}
+ disabled={disabled}>
+ {({hovered, focused, pressed}) => (
- {!disabled && (
-
- {settingDesc}
+
+ {labelStrings.name}
+
+ {labelStrings.description}
- )}
- {!cantConfigure && (
-
+
+
+ {!disabled && (
+
+
+ {settingDesc}
+
+
+
)}
)}
-
+
{labelStrings.name}
@@ -144,10 +169,21 @@ export function ModerationLabelPref({
{labelStrings.description}
+
{cantConfigure && (
-
+
-
+
+
{adultDisabled ? (
Adult content must be enabled to configure this label.
@@ -167,73 +203,47 @@ export function ModerationLabelPref({
)}
- {disabled || cantConfigure ? (
-
- ) : (
- <>
-
- {canWarn && (
-
- )}
+ {!cantConfigure && (
+
+ type="radio"
+ values={selected}
+ onChange={onSettingChange}
+ label={_(
+ msg`Configure filtering settings for ${labelStrings.name}`,
+ )}>
+
+ {settings.map((s, i) => (
+ <>
+ {i !== 0 && }
-
- >
+
+
+
+ {s.label}
+
+
+ >
+ ))}
+
+
)}
+
+
>
diff --git a/src/lib/moderation/useGlobalLabelStrings.ts b/src/lib/moderation/useGlobalLabelStrings.ts
index ab5ab18466..6b936723dd 100644
--- a/src/lib/moderation/useGlobalLabelStrings.ts
+++ b/src/lib/moderation/useGlobalLabelStrings.ts
@@ -18,12 +18,6 @@ export function useGlobalLabelStrings(): GlobalLabelStrings {
name: _(msg`Content Blocked`),
description: _(msg`This content has been hidden by the moderators.`),
},
- '!no-promote': {
- name: _(msg`Moderator Filter`),
- description: _(
- msg`Moderator has chosen to filter the content from feeds.`,
- ),
- },
'!warn': {
name: _(msg`Content Warning`),
description: _(
@@ -36,18 +30,6 @@ export function useGlobalLabelStrings(): GlobalLabelStrings {
msg`This user has requested that their content only be shown to signed-in users.`,
),
},
- 'dmca-violation': {
- name: _(msg`Copyright Violation`),
- description: _(
- msg`This content has received a DMCA takedown request. It will be restored if the concerns can be resolved.`,
- ),
- },
- doxxing: {
- name: _(msg`Doxxing`),
- description: _(
- msg`This content has been reported to include private information about someone without their consent.`,
- ),
- },
porn: {
name: _(msg`Pornography`),
description: _(msg`Explicit sexual images.`),
diff --git a/src/screens/Moderation/index.tsx b/src/screens/Moderation/index.tsx
index c53f482918..bbce303ecc 100644
--- a/src/screens/Moderation/index.tsx
+++ b/src/screens/Moderation/index.tsx
@@ -25,22 +25,25 @@ import {
usePreferencesSetAdultContentMutation,
} from '#/state/queries/preferences'
import {useLabelersDetailedInfoQuery} from '#/state/queries/labeler'
-import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
+import {logger} from '#/logger'
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 {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
+import {Filter_Stroke2_Corner0_Rounded as Filter} from '#/components/icons/Filter'
import {Text} from '#/components/Typography'
import * as Toggle from '#/components/forms/Toggle'
-import {Button} from '#/components/Button'
import {InlineLink, Link} from '#/components/Link'
+import {Button, ButtonText} from '#/components/Button'
import {Loader} from '#/components/Loader'
import {getModerationServiceTitle} from '#/lib/moderation'
import * as ModerationServiceCard from '#/components/ModerationServiceCard'
import {GlobalModerationLabelPref} from '#/components/moderation/GlobalModerationLabelPref'
+import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
+import {useModalControls} from '#/state/modals'
function ErrorState({error}: {error: string}) {
const t = useTheme()
@@ -83,14 +86,15 @@ export function ModerationScreen(
error: preferencesError,
data: preferences,
} = usePreferencesQuery()
+ const labelerDids = preferences
+ ? preferences.moderationPrefs.labelers.map(l => l.did)
+ : []
const {
isLoading: isLabelersLoading,
data: labelers,
error: labelersError,
} = useLabelersDetailedInfoQuery({
- dids: preferences
- ? preferences.moderationPrefs.labelers.map(l => l.did)
- : [],
+ dids: labelerDids,
})
const {gtMobile} = useBreakpoints()
const {height} = useSafeAreaFrame()
@@ -113,7 +117,7 @@ export function ModerationScreen(
- ) : error || !(preferences && labelers) ? (
+ ) : error || !preferences ? (
{
@@ -154,6 +159,8 @@ export function ModerationScreenInner({
(optimisticAdultContent && optimisticAdultContent.enabled) ||
(!optimisticAdultContent && preferences.moderationPrefs.adultContentEnabled)
)
+ const ageNotSet = !preferences.userAge
+ const isUnderage = (preferences.userAge || 0) < 18
const onToggleAdultContentEnabled = React.useCallback(
async (selected: boolean) => {
@@ -161,9 +168,10 @@ export function ModerationScreenInner({
await setAdultContentPref({
enabled: selected,
})
- } catch (e) {
- // TODO
- console.error(e)
+ } catch (e: any) {
+ logger.error(`Failed to set adult content pref`, {
+ message: e.message,
+ })
}
},
[setAdultContentPref],
@@ -191,7 +199,8 @@ export function ModerationScreenInner({
t.atoms.bg_contrast_25,
]}>