Merge branch '3p-moderators' of github.com:bluesky-social/social-app into 3p-moderators
This commit is contained in:
@@ -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 |
@@ -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') {
|
||||||
|
|||||||
@@ -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',
|
||||||
|
})
|
||||||
@@ -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',
|
||||||
|
})
|
||||||
@@ -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.`),
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user