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

This commit is contained in:
Paul Frazee
2024-03-09 12:47:36 -08:00
9 changed files with 296 additions and 225 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="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" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+6 -6
View File
@@ -49,12 +49,12 @@ const GroupContext = React.createContext<{
setFieldValue: () => {}, setFieldValue: () => {},
}) })
export type GroupProps = React.PropsWithChildren<{ export type GroupProps<T = string> = React.PropsWithChildren<{
type?: 'radio' | 'checkbox' type?: 'radio' | 'checkbox'
values: string[] values: T[]
maxSelections?: number maxSelections?: number
disabled?: boolean disabled?: boolean
onChange: (value: string[]) => void onChange: (value: T[]) => void
label: string label: string
}> }>
@@ -73,7 +73,7 @@ export function useItemContext() {
return React.useContext(ItemContext) return React.useContext(ItemContext)
} }
export function Group({ export function Group<ValuesType = string>({
children, children,
values: providedValues, values: providedValues,
onChange, onChange,
@@ -81,13 +81,13 @@ export function Group({
type = 'checkbox', type = 'checkbox',
maxSelections, maxSelections,
label, label,
}: GroupProps) { }: GroupProps<ValuesType>) {
const groupRole = type === 'radio' ? 'radiogroup' : undefined const groupRole = type === 'radio' ? 'radiogroup' : undefined
const values = type === 'radio' ? providedValues.slice(0, 1) : providedValues const values = type === 'radio' ? providedValues.slice(0, 1) : providedValues
const [maxReached, setMaxReached] = React.useState(false) const [maxReached, setMaxReached] = React.useState(false)
const setFieldValue = React.useCallback< const setFieldValue = React.useCallback<
(props: {name: string; value: boolean}) => void (props: {name: ValuesType; value: boolean}) => void
>( >(
({name, value}) => { ({name, value}) => {
if (type === 'checkbox') { if (type === 'checkbox') {
+5
View File
@@ -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',
})
+5
View File
@@ -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',
})
+119 -109
View File
@@ -1,5 +1,5 @@
import React from 'react' import React from 'react'
import {Pressable, View} from 'react-native' import {View} from 'react-native'
import {InterpretedLabelValueDefinition, LabelPreference} from '@atproto/api' import {InterpretedLabelValueDefinition, LabelPreference} from '@atproto/api'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
@@ -19,10 +19,11 @@ import {useTheme, atoms as a} from '#/alf'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {InlineLink} from '#/components/Link' import {InlineLink} from '#/components/Link'
import * as Dialog from '#/components/Dialog' import * as Dialog from '#/components/Dialog'
import {Button, ButtonText, ButtonIcon} from '#/components/Button' import {Button} from '#/components/Button'
import {ArrowTriangleBottom_Stroke2_Corner1_Rounded as ArrowTriangleBottom} from '../icons/ArrowTriangle'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '../icons/CircleInfo' 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({ export function ModerationLabelPref({
labelValueDefinition, labelValueDefinition,
@@ -51,6 +52,7 @@ export function ModerationLabelPref({
savedPref ?? savedPref ??
labelValueDefinition.defaultSetting ?? labelValueDefinition.defaultSetting ??
'warn' 'warn'
const [selected, setSelected] = React.useState<LabelPreference[]>([pref])
const settingDesc = useLabelBehaviorDescription(labelValueDefinition, pref) const settingDesc = useLabelBehaviorDescription(labelValueDefinition, pref)
const hideLabel = useLabelLongBehaviorDescription( const hideLabel = useLabelLongBehaviorDescription(
@@ -81,62 +83,85 @@ export function ModerationLabelPref({
adultOnly && !preferences?.moderationPrefs.adultContentEnabled adultOnly && !preferences?.moderationPrefs.adultContentEnabled
const cantConfigure = isGlobalLabel || adultDisabled const cantConfigure = isGlobalLabel || adultDisabled
const onSelectPref = (newPref: LabelPreference) => const onSettingChange = React.useCallback(
mutate({label: identifier, visibility: newPref, labelerDid}) (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 ( return (
<> <>
<Pressable <Button
onPress={() => control.open()} onPress={() => control.open()}
accessibilityLabel={settingDesc} label={settingDesc}
accessibilityHint="" disabled={disabled}>
style={[ {({hovered, focused, pressed}) => (
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,
]}>
<Text style={[a.font_bold]}>{labelStrings.name}</Text>
<Text
style={[t.atoms.text_contrast_medium, a.flex_1]}
numberOfLines={1}>
{labelStrings.description}
</Text>
{!disabled && (
<View <View
style={[ style={[
{width: 100}, a.w_full,
a.flex_row, a.flex_row,
a.align_center, a.justify_between,
a.justify_end, a.gap_sm,
a.gap_xs, a.align_start,
a.rounded_2xs, a.px_lg,
a.py_lg,
a.rounded_sm,
t.atoms.bg_contrast_25,
(hovered || focused || pressed) && [t.atoms.bg_contrast_50],
]}> ]}>
{!disabled && ( <View style={[a.gap_xs]}>
<Text style={[t.atoms.text_contrast_medium, a.font_semibold]}> <Text style={[a.font_bold]}>{labelStrings.name}</Text>
{settingDesc} <Text
style={[t.atoms.text_contrast_medium, a.leading_snug]}
numberOfLines={1}>
{labelStrings.description}
</Text> </Text>
)} </View>
{!cantConfigure && (
<ArrowTriangleBottom {!disabled && (
width={8} <View
fill={t.atoms.text_contrast_medium.color} style={[
/> a.flex_row,
a.align_center,
a.justify_end,
a.gap_xs,
a.rounded_2xs,
]}>
<Text style={[t.atoms.text_contrast_medium, a.font_semibold]}>
{settingDesc}
</Text>
<Gear size="sm" fill={t.atoms.text_contrast_low.color} />
</View>
)} )}
</View> </View>
)} )}
</Pressable> </Button>
<Dialog.Outer control={control}> <Dialog.Outer control={control}>
<Dialog.Handle /> <Dialog.Handle />
<Dialog.Inner <Dialog.Inner
label={_(msg`Settings: ${labelValueDefinition.identifier}`)} label={_(msg`Settings for ${labelValueDefinition.identifier}`)}
style={[a.gap_sm]}> style={[a.gap_sm]}>
<Text style={[a.text_2xl, a.font_bold, a.pb_xs, t.atoms.text]}> <Text style={[a.text_2xl, a.font_bold, a.pb_xs, t.atoms.text]}>
{labelStrings.name} {labelStrings.name}
@@ -144,10 +169,21 @@ export function ModerationLabelPref({
<Text style={[a.text_md, a.pb_sm, t.atoms.text_contrast_medium]}> <Text style={[a.text_md, a.pb_sm, t.atoms.text_contrast_medium]}>
{labelStrings.description} {labelStrings.description}
</Text> </Text>
{cantConfigure && ( {cantConfigure && (
<View style={[a.flex_row, a.gap_xs, a.align_center, a.pb_sm]}> <View
style={[
a.flex_row,
a.gap_xs,
a.align_center,
a.py_md,
a.px_lg,
a.rounded_sm,
t.atoms.bg_contrast_25,
]}>
<CircleInfo size="md" fill={t.atoms.text_contrast_medium.color} /> <CircleInfo size="md" fill={t.atoms.text_contrast_medium.color} />
<Text style={[a.text_md, t.atoms.text_contrast_medium]}>
<Text style={[t.atoms.text_contrast_medium]}>
{adultDisabled ? ( {adultDisabled ? (
<Trans> <Trans>
Adult content must be enabled to configure this label. Adult content must be enabled to configure this label.
@@ -167,73 +203,47 @@ export function ModerationLabelPref({
</Text> </Text>
</View> </View>
)} )}
{disabled || cantConfigure ? (
<Button
label={_(msg`Close`)}
size="large"
variant="solid"
color="secondary"
onPress={() => control.close()}>
<ButtonText style={[a.flex_1, a.text_left]}>
<Trans>Close</Trans>
</ButtonText>
</Button>
) : (
<>
<Button
label={hideLabel}
size="large"
variant="solid"
color={pref === 'hide' ? 'primary' : 'secondary'}
onPress={() => {
onSelectPref('hide')
control.close()
}}>
<ButtonText style={[a.flex_1, a.text_left]}>
{hideLabel}
</ButtonText>
{pref === 'hide' && (
<ButtonIcon icon={Check} position="right" />
)}
</Button>
{canWarn && ( {!cantConfigure && (
<Button <Toggle.Group<LabelPreference>
label={warnLabel} type="radio"
size="large" values={selected}
variant="solid" onChange={onSettingChange}
color={pref === 'warn' ? 'primary' : 'secondary'} label={_(
onPress={() => { msg`Configure filtering settings for ${labelStrings.name}`,
onSelectPref('warn') )}>
control.close() <View
}}> style={[
<ButtonText style={[a.flex_1, a.text_left]}> a.rounded_md,
{warnLabel} a.overflow_hidden,
</ButtonText> t.atoms.bg_contrast_25,
{pref === 'warn' && ( ]}>
<ButtonIcon icon={Check} position="right" /> {settings.map((s, i) => (
)} <>
</Button> {i !== 0 && <Divider />}
)}
<Button <Toggle.Item name={s.pref} label={s.label}>
label={ignoreLabel} <View
size="large" style={[
variant="solid" a.flex_1,
color={!disabled && pref === 'ignore' ? 'primary' : 'secondary'} a.flex_row,
onPress={() => { a.align_center,
onSelectPref('ignore') a.gap_md,
control.close() a.py_md,
}}> a.px_lg,
<ButtonText style={[a.flex_1, a.text_left]}> t.atoms.bg_contrast_25,
{ignoreLabel} ]}>
</ButtonText> <Toggle.Radio />
{pref === 'ignore' && ( <Text>{s.label}</Text>
<ButtonIcon icon={Check} position="right" /> </View>
)} </Toggle.Item>
</Button> </>
</> ))}
</View>
</Toggle.Group>
)} )}
<Dialog.Close />
</Dialog.Inner> </Dialog.Inner>
</Dialog.Outer> </Dialog.Outer>
</> </>
@@ -18,12 +18,6 @@ export function useGlobalLabelStrings(): GlobalLabelStrings {
name: _(msg`Content Blocked`), name: _(msg`Content Blocked`),
description: _(msg`This content has been hidden by the moderators.`), 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': { '!warn': {
name: _(msg`Content Warning`), name: _(msg`Content Warning`),
description: _( description: _(
@@ -36,18 +30,6 @@ export function useGlobalLabelStrings(): GlobalLabelStrings {
msg`This user has requested that their content only be shown to signed-in users.`, 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: { porn: {
name: _(msg`Pornography`), name: _(msg`Pornography`),
description: _(msg`Explicit sexual images.`), description: _(msg`Explicit sexual images.`),
+135 -69
View File
@@ -25,22 +25,25 @@ import {
usePreferencesSetAdultContentMutation, usePreferencesSetAdultContentMutation,
} from '#/state/queries/preferences' } from '#/state/queries/preferences'
import {useLabelersDetailedInfoQuery} from '#/state/queries/labeler' 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 {useTheme, atoms as a, useBreakpoints} from '#/alf'
import {Divider} from '#/components/Divider' import {Divider} from '#/components/Divider'
import {CircleBanSign_Stroke2_Corner0_Rounded as CircleBanSign} from '#/components/icons/CircleBanSign' import {CircleBanSign_Stroke2_Corner0_Rounded as CircleBanSign} from '#/components/icons/CircleBanSign'
import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group' import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group'
import {Person_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person' import {Person_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person'
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron' 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 {Text} from '#/components/Typography'
import * as Toggle from '#/components/forms/Toggle' import * as Toggle from '#/components/forms/Toggle'
import {Button} from '#/components/Button'
import {InlineLink, Link} from '#/components/Link' import {InlineLink, Link} from '#/components/Link'
import {Button, ButtonText} from '#/components/Button'
import {Loader} from '#/components/Loader' import {Loader} from '#/components/Loader'
import {getModerationServiceTitle} from '#/lib/moderation' import {getModerationServiceTitle} from '#/lib/moderation'
import * as ModerationServiceCard from '#/components/ModerationServiceCard' import * as ModerationServiceCard from '#/components/ModerationServiceCard'
import {GlobalModerationLabelPref} from '#/components/moderation/GlobalModerationLabelPref' import {GlobalModerationLabelPref} from '#/components/moderation/GlobalModerationLabelPref'
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
import {useModalControls} from '#/state/modals'
function ErrorState({error}: {error: string}) { function ErrorState({error}: {error: string}) {
const t = useTheme() const t = useTheme()
@@ -83,14 +86,15 @@ export function ModerationScreen(
error: preferencesError, error: preferencesError,
data: preferences, data: preferences,
} = usePreferencesQuery() } = usePreferencesQuery()
const labelerDids = preferences
? preferences.moderationPrefs.labelers.map(l => l.did)
: []
const { const {
isLoading: isLabelersLoading, isLoading: isLabelersLoading,
data: labelers, data: labelers,
error: labelersError, error: labelersError,
} = useLabelersDetailedInfoQuery({ } = useLabelersDetailedInfoQuery({
dids: preferences dids: labelerDids,
? preferences.moderationPrefs.labelers.map(l => l.did)
: [],
}) })
const {gtMobile} = useBreakpoints() const {gtMobile} = useBreakpoints()
const {height} = useSafeAreaFrame() const {height} = useSafeAreaFrame()
@@ -113,7 +117,7 @@ export function ModerationScreen(
<View style={[a.w_full, a.align_center, a.pt_2xl]}> <View style={[a.w_full, a.align_center, a.pt_2xl]}>
<Loader size="xl" fill={t.atoms.text.color} /> <Loader size="xl" fill={t.atoms.text.color} />
</View> </View>
) : error || !(preferences && labelers) ? ( ) : error || !preferences ? (
<ErrorState <ErrorState
error={ error={
preferencesError?.toString() || preferencesError?.toString() ||
@@ -129,10 +133,10 @@ export function ModerationScreen(
export function ModerationScreenInner({ export function ModerationScreenInner({
preferences, preferences,
labelers, labelers = [],
}: { }: {
preferences: UsePreferencesQueryResponse preferences: UsePreferencesQueryResponse
labelers: AppBskyLabelerDefs.LabelerViewDetailed[] labelers?: AppBskyLabelerDefs.LabelerViewDetailed[]
}) { }) {
const {_} = useLingui() const {_} = useLingui()
const t = useTheme() const t = useTheme()
@@ -140,6 +144,7 @@ export function ModerationScreenInner({
const {screen} = useAnalytics() const {screen} = useAnalytics()
const {gtMobile} = useBreakpoints() const {gtMobile} = useBreakpoints()
const {mutedWordsDialogControl} = useGlobalDialogsControlContext() const {mutedWordsDialogControl} = useGlobalDialogsControlContext()
const {openModal} = useModalControls()
useFocusEffect( useFocusEffect(
React.useCallback(() => { React.useCallback(() => {
@@ -154,6 +159,8 @@ export function ModerationScreenInner({
(optimisticAdultContent && optimisticAdultContent.enabled) || (optimisticAdultContent && optimisticAdultContent.enabled) ||
(!optimisticAdultContent && preferences.moderationPrefs.adultContentEnabled) (!optimisticAdultContent && preferences.moderationPrefs.adultContentEnabled)
) )
const ageNotSet = !preferences.userAge
const isUnderage = (preferences.userAge || 0) < 18
const onToggleAdultContentEnabled = React.useCallback( const onToggleAdultContentEnabled = React.useCallback(
async (selected: boolean) => { async (selected: boolean) => {
@@ -161,9 +168,10 @@ export function ModerationScreenInner({
await setAdultContentPref({ await setAdultContentPref({
enabled: selected, enabled: selected,
}) })
} catch (e) { } catch (e: any) {
// TODO logger.error(`Failed to set adult content pref`, {
console.error(e) message: e.message,
})
} }
}, },
[setAdultContentPref], [setAdultContentPref],
@@ -191,7 +199,8 @@ export function ModerationScreenInner({
t.atoms.bg_contrast_25, t.atoms.bg_contrast_25,
]}> ]}>
<Button <Button
testID="mutewordsBtn" testID="mutedWordsBtn"
label={_(msg`Open muted words and tags settings`)}
style={[ style={[
a.flex_row, a.flex_row,
a.align_center, a.align_center,
@@ -199,11 +208,10 @@ export function ModerationScreenInner({
a.p_lg, a.p_lg,
a.gap_sm, a.gap_sm,
]} ]}
label={_(msg`Muted words & tags`)}
onPress={() => mutedWordsDialogControl.open()}> onPress={() => mutedWordsDialogControl.open()}>
<View style={[a.flex_row, a.align_center, a.gap_md]}> <View style={[a.flex_row, a.align_center, a.gap_md]}>
<Group size="md" style={[t.atoms.text_contrast_medium]} /> <Filter size="md" style={[t.atoms.text_contrast_medium]} />
<Text style={[a.text_md]}> <Text style={[a.text_sm, a.font_bold]}>
<Trans>Muted words & tags</Trans> <Trans>Muted words & tags</Trans>
</Text> </Text>
</View> </View>
@@ -225,7 +233,7 @@ export function ModerationScreenInner({
to="/moderation/modlists"> to="/moderation/modlists">
<View style={[a.flex_row, a.align_center, a.gap_md]}> <View style={[a.flex_row, a.align_center, a.gap_md]}>
<Group size="md" style={[t.atoms.text_contrast_medium]} /> <Group size="md" style={[t.atoms.text_contrast_medium]} />
<Text style={[a.text_md]}> <Text style={[a.text_sm, a.font_bold]}>
<Trans>Moderation lists</Trans> <Trans>Moderation lists</Trans>
</Text> </Text>
</View> </View>
@@ -247,7 +255,7 @@ export function ModerationScreenInner({
to="/moderation/muted-accounts"> to="/moderation/muted-accounts">
<View style={[a.flex_row, a.align_center, a.gap_md]}> <View style={[a.flex_row, a.align_center, a.gap_md]}>
<Person size="md" style={[t.atoms.text_contrast_medium]} /> <Person size="md" style={[t.atoms.text_contrast_medium]} />
<Text style={[a.text_md]}> <Text style={[a.text_sm, a.font_bold]}>
<Trans>Muted accounts</Trans> <Trans>Muted accounts</Trans>
</Text> </Text>
</View> </View>
@@ -269,7 +277,7 @@ export function ModerationScreenInner({
to="/moderation/blocked-accounts"> to="/moderation/blocked-accounts">
<View style={[a.flex_row, a.align_center, a.gap_md]}> <View style={[a.flex_row, a.align_center, a.gap_md]}>
<CircleBanSign size="md" style={[t.atoms.text_contrast_medium]} /> <CircleBanSign size="md" style={[t.atoms.text_contrast_medium]} />
<Text style={[a.text_md]}> <Text style={[a.text_sm, a.font_bold]}>
<Trans>Blocked accounts</Trans> <Trans>Blocked accounts</Trans>
</Text> </Text>
</View> </View>
@@ -280,59 +288,117 @@ export function ModerationScreenInner({
</Link> </Link>
</View> </View>
<Text {ageNotSet || !isUnderage ? (
style={[ <>
a.pt_2xl,
a.pb_md,
a.text_md,
a.font_bold,
t.atoms.text_contrast_high,
]}>
<Trans>Content filters</Trans>
</Text>
<View
style={[
a.w_full,
a.rounded_md,
a.overflow_hidden,
t.atoms.bg_contrast_25,
]}>
<View
style={[a.pt_lg, a.pb_md, a.px_lg, a.flex_row, a.justify_between]}>
<Text <Text
style={[a.text_md, a.font_semibold, t.atoms.text_contrast_high]}> style={[
<Trans>Enable adult content</Trans> a.pt_2xl,
a.pb_md,
a.text_md,
a.font_bold,
t.atoms.text_contrast_high,
]}>
<Trans>Content filters</Trans>
</Text> </Text>
<Toggle.Item
label={'Toggle adult content'} <View style={[a.gap_md]}>
name="adultContent" {ageNotSet ? (
value={adultContentEnabled} <>
onChange={onToggleAdultContentEnabled}> <View
<View style={[a.flex_row, a.align_center, a.gap_sm]}> style={[
<Text style={[t.atoms.text_contrast_medium]}> a.w_full,
{adultContentEnabled ? ( a.rounded_md,
<Trans>Enabled</Trans> a.overflow_hidden,
) : ( t.atoms.bg_contrast_25,
<Trans>Disabled</Trans> a.py_md,
)} a.px_lg,
</Text> a.flex_row,
<Toggle.Switch /> a.align_center,
</View> a.justify_between,
</Toggle.Item> ]}>
</View> <Text style={[a.text_md, t.atoms.text_contrast_high]}>
{adultContentEnabled && ( <Trans>Confirm your age:</Trans>
<> </Text>
<Divider /> <Button
<GlobalModerationLabelPref labelValueDefinition={LABELS.porn} /> label={_(msg`Confirm your birthdate`)}
<Divider /> size="small"
<GlobalModerationLabelPref labelValueDefinition={LABELS.sexual} /> variant="solid"
<Divider /> color="secondary"
<GlobalModerationLabelPref labelValueDefinition={LABELS.nudity} /> onPress={() => {
<Divider /> openModal({name: 'birth-date-settings'})
<GlobalModerationLabelPref labelValueDefinition={LABELS.gore} /> }}>
</> <ButtonText>
)} <Trans>Set birthdate</Trans>
</View> </ButtonText>
</Button>
</View>
</>
) : (
<>
<View
style={[
a.w_full,
a.rounded_md,
a.overflow_hidden,
t.atoms.bg_contrast_25,
]}>
<View
style={[
a.py_lg,
a.px_lg,
a.flex_row,
a.align_center,
a.justify_between,
]}>
<Text
style={[a.font_semibold, t.atoms.text_contrast_high]}>
<Trans>Enable adult content</Trans>
</Text>
<Toggle.Item
label={_(
msg`Toggle to enable or disable adult content`,
)}
name="adultContent"
value={adultContentEnabled}
onChange={onToggleAdultContentEnabled}>
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
<Text style={[t.atoms.text_contrast_medium]}>
{adultContentEnabled ? (
<Trans>Enabled</Trans>
) : (
<Trans>Disabled</Trans>
)}
</Text>
<Toggle.Switch />
</View>
</Toggle.Item>
</View>
{adultContentEnabled && (
<>
<Divider />
<GlobalModerationLabelPref
labelValueDefinition={LABELS.porn}
/>
<Divider />
<GlobalModerationLabelPref
labelValueDefinition={LABELS.sexual}
/>
<Divider />
<GlobalModerationLabelPref
labelValueDefinition={LABELS.nudity}
/>
<Divider />
<GlobalModerationLabelPref
labelValueDefinition={LABELS.gore}
/>
</>
)}
</View>
</>
)}
</View>
</>
) : null}
<Text <Text
style={[ style={[
@@ -9,7 +9,7 @@ import {
} from '@atproto/api' } from '@atproto/api'
import {Trans, msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {RichText} from 'view/com/util/text/RichText' import {RichText} from '#/components/RichText'
import {useModalControls} from '#/state/modals' import {useModalControls} from '#/state/modals'
import {usePreferencesQuery} from '#/state/queries/preferences' import {usePreferencesQuery} from '#/state/queries/preferences'
import {useAnalytics} from 'lib/analytics/analytics' import {useAnalytics} from 'lib/analytics/analytics'
@@ -24,6 +24,7 @@ import {pluralize} from '#/lib/strings/helpers'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button' import {Button, ButtonText} from '#/components/Button'
import {Text} from '#/components/Typography'
import * as Toast from '#/view/com/util/Toast' import * as Toast from '#/view/com/util/Toast'
import {ProfileHeaderShell} from './Shell' import {ProfileHeaderShell} from './Shell'
import {ProfileHeaderDropdownBtn} from './DropdownBtn' import {ProfileHeaderDropdownBtn} from './DropdownBtn'
@@ -86,6 +87,8 @@ let ProfileHeaderLabeler = ({
labeler.viewer?.like || '', labeler.viewer?.like || '',
) )
const isLiked = !!likeUri const isLiked = !!likeUri
const likeCount =
isLiked && likeUri ? (labeler.likeCount || 0) + 1 : labeler.likeCount || 0
const onToggleLiked = React.useCallback(async () => { const onToggleLiked = React.useCallback(async () => {
if (!labeler) { if (!labeler) {
@@ -210,13 +213,13 @@ let ProfileHeaderLabeler = ({
<View pointerEvents="auto"> <View pointerEvents="auto">
<RichText <RichText
testID="profileHeaderDescription" testID="profileHeaderDescription"
style={t.atoms.text} style={[a.text_md]}
numberOfLines={15} numberOfLines={15}
richText={descriptionRT} value={descriptionRT}
/> />
</View> </View>
) : undefined} ) : undefined}
<View style={[a.flex_row, a.gap_xs, a.align_center, a.pt_md]}> <View style={[a.flex_row, a.gap_xs, a.align_center, a.pt_lg]}>
<Button <Button
testID="toggleLikeBtn" testID="toggleLikeBtn"
size="small" size="small"
@@ -235,26 +238,25 @@ let ProfileHeaderLabeler = ({
{typeof labeler.likeCount === 'number' && ( {typeof labeler.likeCount === 'number' && (
<Button <Button
variant="ghost"
size="tiny" size="tiny"
onPress={() => likesControl.open()} onPress={() => likesControl.open()}
label={_( label={_(
msg`Liked by ${labeler.likeCount} ${pluralize( msg`Liked by ${likeCount} ${pluralize(likeCount, 'user')}`,
labeler.likeCount,
'user',
)}`,
)}> )}>
<ButtonText {({hovered, focused, pressed}) => (
style={[ <Text
t.atoms.text_contrast_medium, style={[
a.font_bold, a.font_bold,
a.text_sm, a.text_sm,
]}> t.atoms.text_contrast_medium,
<Trans> (hovered || focused || pressed) &&
Liked by {labeler.likeCount}{' '} t.atoms.text_contrast_high,
{pluralize(labeler.likeCount, 'user')} ]}>
</Trans> <Trans>
</ButtonText> Liked by {likeCount} {pluralize(likeCount, 'user')}
</Trans>
</Text>
)}
</Button> </Button>
)} )}
</View> </View>
@@ -31,7 +31,7 @@ import {ProfileHeaderDisplayName} from './DisplayName'
import {ProfileHeaderHandle} from './Handle' import {ProfileHeaderHandle} from './Handle'
import {ProfileHeaderMetrics} from './Metrics' import {ProfileHeaderMetrics} from './Metrics'
import {ProfileHeaderSuggestedFollows} from '#/view/com/profile/ProfileHeaderSuggestedFollows' import {ProfileHeaderSuggestedFollows} from '#/view/com/profile/ProfileHeaderSuggestedFollows'
import {RichText} from 'view/com/util/text/RichText' import {RichText} from '#/components/RichText'
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
@@ -249,9 +249,9 @@ let ProfileHeaderStandard = ({
<View pointerEvents="auto"> <View pointerEvents="auto">
<RichText <RichText
testID="profileHeaderDescription" testID="profileHeaderDescription"
style={t.atoms.text} style={[a.text_md]}
numberOfLines={15} numberOfLines={15}
richText={descriptionRT} value={descriptionRT}
/> />
</View> </View>
) : undefined} ) : undefined}