Implement label handling

This commit is contained in:
Paul Frazee
2024-03-05 16:18:49 -08:00
parent e794e84b48
commit a84e264dae
28 changed files with 677 additions and 664 deletions
@@ -4,7 +4,7 @@ import {InterprettedLabelValueDefinition, LabelPreference} from '@atproto/api'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
import {useLabelStrings} from '#/lib/moderation/useLabelStrings'
import {useGlobalLabelStrings} from '#/lib/moderation/useGlobalLabelStrings'
import {
usePreferencesQuery,
usePreferencesSetContentLabelMutation,
@@ -30,10 +30,10 @@ export function SimpleModerationLabelPref({
const savedPref = preferences?.moderationPrefs.labels[identifier]
const pref = variables?.visibility ?? savedPref ?? 'warn'
const allLabelStrings = useLabelStrings()
const allLabelStrings = useGlobalLabelStrings()
const labelStrings =
labelValueDefinition.identifier in allLabelStrings
? allLabelStrings[labelValueDefinition.identifier].general
? allLabelStrings[labelValueDefinition.identifier]
: {
name: labelValueDefinition.identifier,
description: `Labeled "${labelValueDefinition.identifier}"`,
+3 -3
View File
@@ -4,7 +4,7 @@ import {InterprettedLabelValueDefinition, LabelPreference} from '@atproto/api'
import {useLingui} from '@lingui/react'
import {msg, Trans} from '@lingui/macro'
import {useLabelStrings} from '#/lib/moderation/useLabelStrings'
import {useGlobalLabelStrings} from '#/lib/moderation/useGlobalLabelStrings'
import {
useLabelBehaviorDescription,
useLabelLongBehaviorDescription,
@@ -58,11 +58,11 @@ export function ModerationLabelPref({
'ignore',
)
const allLabelStrings = useLabelStrings()
const allLabelStrings = useGlobalLabelStrings()
const labelStrings = labelValueDefinition.locales[0] // TODO look up locale
? labelValueDefinition.locales[0]
: labelValueDefinition.identifier in allLabelStrings
? allLabelStrings[labelValueDefinition.identifier].general
? allLabelStrings[labelValueDefinition.identifier]
: {
name: labelValueDefinition.identifier,
description: `Labeled "${labelValueDefinition.identifier}"`,
@@ -11,12 +11,12 @@ import {useLabelerInfoQuery} from '#/state/queries/labeler'
export * as Card from '#/components/ModerationServiceCard/Card'
type ModerationServiceProps = {
modservice: AppBskyLabelerDefs.LabelerViewDetailed
labeler: AppBskyLabelerDefs.LabelerViewDetailed
}
export function Link({
children,
modservice,
labeler,
}: ModerationServiceProps & Pick<LinkProps, 'children'>) {
const {_} = useLingui()
@@ -25,11 +25,11 @@ export function Link({
to={{
screen: 'Profile',
params: {
name: modservice.creator.handle,
name: labeler.creator.handle,
},
}}
label={_(
msg`View the moderation service provided by @${modservice.creator.handle}`,
msg`View the moderation service provided by @${labeler.creator.handle}`,
)}>
{children}
</InternalLink>
@@ -54,7 +54,7 @@ export function Loader({
loading?: React.ComponentType<{}>
error?: React.ComponentType<{error: string}>
component: React.ComponentType<{
modservice: AppBskyLabelerDefs.LabelerViewDetailed
labeler: AppBskyLabelerDefs.LabelerViewDetailed
}>
}) {
const {isLoading, data, error} = useLabelerInfoQuery({did})
@@ -68,6 +68,6 @@ export function Loader({
<ErrorComponent error={error?.message || 'Unknown error'} />
) : null
) : (
<Component modservice={data} />
<Component labeler={data} />
)
}
+13 -13
View File
@@ -112,7 +112,7 @@ function LabelGroupButton({
)
}
function ModServiceToggle({title}: {title: string}) {
function LabelerToggle({title}: {title: string}) {
const t = useTheme()
const ctx = Toggle.useItemContext()
@@ -260,7 +260,7 @@ function SubmitView({
key={labeler.creator.did}
name={labeler.creator.did}
label={title}>
<ModServiceToggle title={title} />
<LabelerToggle title={title} />
</Toggle.Item>
)
})}
@@ -477,21 +477,21 @@ function ReportDialogInner(props: ReportDialogProps) {
data: preferences,
} = usePreferencesQuery()
const {
isLoading: isModServicesLoading,
data: modservices,
error: modservicesError,
isLoading: isLabelersLoading,
data: labelers,
error: labelersError,
} = useLabelersDetailedInfoQuery({
dids: preferences ? preferences.moderationOpts.mods.map(m => m.did) : [],
dids: preferences ? preferences.moderationPrefs.mods.map(m => m.did) : [],
})
const isLoading = isPreferencesLoading || isModServicesLoading
const error = preferencesError || modservicesError
const isLoading = isPreferencesLoading || isLabelersLoading
const error = preferencesError || labelersError
const [fakeLoading, setFakeLoading] = React.useState(isLoading)
const labelGroupToLabelerMap = React.useMemo(() => {
if (!modservices) return {}
return getLabelGroupToLabelerMap(modservices)
}, [modservices])
if (!labelers) return {}
return getLabelGroupToLabelerMap(labelers)
}, [labelers])
React.useEffect(() => {
// on initial load, show a loading spinner for a hot sec to prevent flash
@@ -506,10 +506,10 @@ function ReportDialogInner(props: ReportDialogProps) {
{/* Here to capture focus for a hot sec to prevent flash */}
<Pressable accessible={false} />
</View>
) : error || !(preferences && modservices) ? null : ( // TODO
) : error || !(preferences && labelers) ? null : ( // TODO
<ReportDialogLoaded
{...props}
labelers={modservices}
labelers={labelers}
labelGroupToLabelerMap={labelGroupToLabelerMap}
/>
)}
+130
View File
@@ -0,0 +1,130 @@
import React from 'react'
import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
import {ModerationUI} from '@atproto/api'
import {useLingui} from '@lingui/react'
import {msg, Trans} from '@lingui/macro'
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
import {isJustAMute} from '#/lib/moderation'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {atoms as a, useTheme, useBreakpoints} from '#/alf'
import {Button, ButtonText, ButtonIcon} from '#/components/Button'
import {Text} from '#/components/Typography'
import {
ModerationDetailsDialog,
useModerationDetailsDialogControl,
} from '#/components/moderation/ModerationDetailsDialog'
export function ContentHider({
testID,
modui,
ignoreMute,
style,
childContainerStyle,
children,
}: React.PropsWithChildren<{
testID?: string
modui: ModerationUI | undefined
ignoreMute?: boolean
style?: StyleProp<ViewStyle>
childContainerStyle?: StyleProp<ViewStyle>
}>) {
const t = useTheme()
const {_} = useLingui()
const [override, setOverride] = React.useState(false)
const {gtMobile} = useBreakpoints()
const control = useModerationDetailsDialogControl()
const blur = modui?.blurs[0]
const desc = useModerationCauseDescription(blur)
if (!blur || (ignoreMute && isJustAMute(modui))) {
return (
<View testID={testID} style={[styles.outer, style]}>
{children}
</View>
)
}
return (
<View testID={testID} style={[a.overflow_hidden, style]}>
<ModerationDetailsDialog control={control} modcause={blur} />
<Button
variant="solid"
color="secondary"
size="large"
shape="default"
onPress={() => {
if (!modui.noOverride) {
setOverride(v => !v)
} else {
control.open()
}
}}
label={desc.name}
accessibilityHint={
override ? _(msg`Hide the content`) : _(msg`Show the content`)
}
style={
gtMobile ? [a.py_lg, a.px_xl, a.gap_sm] : [a.py_md, a.px_lg, a.gap_sm]
}>
<ButtonIcon icon={desc.icon} position="left" />{' '}
<ButtonText style={[a.flex_1, a.text_left]}>{desc.name}</ButtonText>
{!modui.noOverride && (
<ButtonText>
{override ? <Trans>Hide</Trans> : <Trans>Show</Trans>}
</ButtonText>
)}
</Button>
{desc.source && blur.type === 'label' && !override && (
<Button
variant="ghost"
size="tiny"
onPress={() => {
control.open()
}}
label={_(msg`Learn more`)}
style={[]}>
<ButtonText
style={[
a.flex_1,
a.text_sm,
a.font_normal,
t.atoms.text_contrast_medium,
a.text_left,
]}>
<Trans>
{sanitizeDisplayName(desc.source)}.{' '}
<Text style={[{color: t.palette.primary_500}, a.text_sm]}>
Learn more.
</Text>
</Trans>
</ButtonText>
</Button>
)}
{override && <View style={childContainerStyle}>{children}</View>}
</View>
)
}
const styles = StyleSheet.create({
outer: {
overflow: 'hidden',
},
cover: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
borderRadius: 8,
marginTop: 4,
paddingVertical: 14,
paddingLeft: 14,
paddingRight: 18,
},
showBtn: {
marginLeft: 'auto',
alignSelf: 'center',
},
})
@@ -11,7 +11,7 @@ import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/ico
import {
LabelsOnMeDialog,
useLabelsOnMeDialogControl,
} from '#/components/LabelsOnMeDialog'
} from '#/components/moderation/LabelsOnMeDialog'
export function LabelsOnMe({
details,
@@ -4,11 +4,15 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {ComAtprotoLabelDefs} from '@atproto/api'
import {useLabelInfo} from '#/lib/moderation/useLabelInfo'
import {makeProfileLink} from '#/lib/routes/links'
import {sanitizeHandle} from '#/lib/strings/handles'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Text} from '#/components/Typography'
import * as Dialog from '#/components/Dialog'
import {Button} from '#/components/Button'
import {capitalize} from '#/lib/strings/capitalize'
import {Button, ButtonText} from '#/components/Button'
import {InlineLink} from '#/components/Link'
export {useDialogControl as useLabelsOnMeDialogControl} from '#/components/Dialog'
@@ -61,12 +65,13 @@ export function LabelsOnMeDialogInner(props: LabelsOnMeDialogProps) {
<Text
nativeID="dialog-title"
style={[a.text_2xl, a.font_bold, a.pb_md, a.leading_tight]}>
<Trans>
The following labels were applied to your{' '}
{isAccount ? 'account' : 'content'}
</Trans>
{isAccount ? (
<Trans>Labels on your account</Trans>
) : (
<Trans>Labels on your content</Trans>
)}
</Text>
<Text nativeID="dialog-description" style={[a.text_md, a.leading_snug]}>
<Text nativeID="dialog-description" style={[a.text_sm, a.leading_snug]}>
<Trans>
You may appeal these labels if you feel they were placed in error.
</Trans>
@@ -74,11 +79,11 @@ export function LabelsOnMeDialogInner(props: LabelsOnMeDialogProps) {
<View style={[a.py_lg, a.gap_md]}>
{labels.map(label => (
<View
key={`${label.src}-${label.val}`}
style={[a.p_md, a.rounded_sm, t.atoms.bg_contrast_25]}>
<Text>{capitalize(label.val)}</Text>
</View>
<Label
key={`${label.val}-${label.src}`}
label={label}
control={props.control}
/>
))}
</View>
@@ -106,3 +111,49 @@ export function LabelsOnMeDialog(props: LabelsOnMeDialogProps) {
</Dialog.Outer>
)
}
function Label({
label,
control,
}: {
label: ComAtprotoLabelDefs.Label
control: Dialog.DialogOuterProps['control']
}) {
const t = useTheme()
const {labeler, strings} = useLabelInfo(label)
return (
<View
key={`${label.src}-${label.val}`}
style={[
a.p_md,
a.rounded_sm,
// t.atoms.bg_contrast_25,
a.border,
t.atoms.border_contrast_low,
a.gap_sm,
a.flex_row,
]}>
<View style={[a.flex_1, a.gap_xs]}>
<Text style={[a.font_bold, a.text_md, t.atoms.text]}>
{strings.name}
</Text>
<Text style={[t.atoms.text]}>{strings.description}</Text>
<InlineLink
to={makeProfileLink(
labeler ? labeler.creator : {did: label.src, handle: ''},
)}
onPress={() => control.close()}
style={[]}>
{labeler ? sanitizeHandle(labeler.creator.handle, '@') : label.src}
</InlineLink>
</View>
<View>
<Button variant="solid" color="secondary" size="small">
<ButtonText>
<Trans>Appeal</Trans>
</ButtonText>
</Button>
</View>
</View>
)
}
@@ -4,19 +4,20 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {ModerationCause} from '@atproto/api'
import {atoms as a, useBreakpoints} from '#/alf'
import {listUriToHref} from '#/lib/strings/url-helpers'
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
import {useTheme, atoms as a, useBreakpoints} from '#/alf'
import {Text} from '#/components/Typography'
import * as Dialog from '#/components/Dialog'
import {Button} from '#/components/Button'
import {InlineLink} from '#/components/Link'
import {useLabelStrings} from '#/lib/moderation/useLabelStrings'
import {listUriToHref} from '#/lib/strings/url-helpers'
import {makeProfileLink} from '#/lib/routes/links'
export {useDialogControl as useModerationDetailsDialogControl} from '#/components/Dialog'
export interface ModerationDetailsDialogProps {
control: Dialog.DialogOuterProps['control']
context: 'account' | 'content'
modcause: ModerationCause
}
@@ -24,21 +25,20 @@ export function ModerationDetailsDialog(props: ModerationDetailsDialogProps) {
return (
<Dialog.Outer control={props.control}>
<Dialog.Handle />
<ModerationDetailsDialogInner {...props} />
</Dialog.Outer>
)
}
function ModerationDetailsDialogInner({
context,
modcause,
control,
}: ModerationDetailsDialogProps & {
control: Dialog.DialogOuterProps['control']
}) {
const t = useTheme()
const {_} = useLingui()
const labelStrings = useLabelStrings()
const control = Dialog.useDialogControl()
const desc = useModerationCauseDescription(modcause)
const {gtMobile} = useBreakpoints()
let name
@@ -95,13 +95,8 @@ function ModerationDetailsDialogInner({
description = _(msg`You have muted this user.`)
}
} else if (modcause.type === 'label') {
if (modcause.labelDef.id in labelStrings) {
name = labelStrings[modcause.labelDef.id][context].name
description = labelStrings[modcause.labelDef.id][context].description
} else {
name = modcause.labelDef.id
description = _(msg`Labeled ${modcause.labelDef.id}`)
}
name = desc.name
description = desc.description
} else {
// should never happen
name = ''
@@ -112,12 +107,38 @@ function ModerationDetailsDialogInner({
<Dialog.ScrollableInner
accessibilityDescribedBy="dialog-description"
accessibilityLabelledBy="dialog-title">
<Text nativeID="dialog-title" style={[a.text_2xl, a.font_bold]}>
<Text
nativeID="dialog-title"
style={[t.atoms.text, a.text_2xl, a.font_bold, a.mb_md]}>
{name}
</Text>
<Text nativeID="dialog-description" style={[a.text_sm]}>
<Text
nativeID="dialog-description"
style={[t.atoms.text, a.text_md, a.mb_md]}>
{description}
</Text>
{modcause.type === 'label' && (
<View
style={[
t.atoms.bg_contrast_50,
a.mb_md,
a.px_lg,
a.py_lg,
a.rounded_sm,
]}>
<Text style={[t.atoms.text, a.text_sm, a.leading_snug]}>
<Trans>
This label was applied by{' '}
<InlineLink
to={makeProfileLink({did: modcause.label.src, handle: ''})}
onPress={() => control.close()}>
{desc.source}
</InlineLink>
.
</Trans>
</Text>
</View>
)}
<View style={gtMobile && [a.flex_row, a.justify_end]}>
<Button
testID="doneBtn"
@@ -1,20 +1,18 @@
import React from 'react'
import {StyleProp, View, ViewStyle} from 'react-native'
import {ModerationUI, ModerationCause} from '@atproto/api'
import {Text} from '../text/Text'
import {Trans} from '@lingui/macro'
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
import {getModerationCauseKey} from '#/lib/moderation'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonText, ButtonIcon} from '#/components/Button'
import {Shield_Stroke2_Corner0_Rounded as Shield} from '#/components/icons/Shield'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash'
import {Text} from '#/components/Typography'
import {
ModerationDetailsDialog,
useModerationDetailsDialogControl,
} from '#/components/ModerationDetailsDialog'
} from '#/components/moderation/ModerationDetailsDialog'
export function PostAlerts({
modui,
@@ -46,7 +44,7 @@ export function PostAlerts({
function PostInform({cause}: {cause: ModerationCause}) {
const control = useModerationDetailsDialogControl()
const desc = useModerationCauseDescription(cause, 'content')
const desc = useModerationCauseDescription(cause)
return (
<>
@@ -59,18 +57,11 @@ function PostInform({cause}: {cause: ModerationCause}) {
onPress={() => {
control.open()
}}>
<ButtonIcon
icon={cause.type === 'muted' ? EyeSlash : CircleInfo}
position="left"
/>{' '}
<ButtonIcon icon={desc.icon} position="left" />{' '}
<ButtonText>{desc.name}</ButtonText>
</Button>
<ModerationDetailsDialog
control={control}
context="content"
modcause={cause}
/>
<ModerationDetailsDialog control={control} modcause={cause} />
</>
)
}
@@ -78,7 +69,7 @@ function PostInform({cause}: {cause: ModerationCause}) {
function PostAlert({cause}: {cause: ModerationCause}) {
const t = useTheme()
const control = useModerationDetailsDialogControl()
const desc = useModerationCauseDescription(cause, 'content')
const desc = useModerationCauseDescription(cause)
return (
<>
@@ -91,21 +82,19 @@ function PostAlert({cause}: {cause: ModerationCause}) {
onPress={() => {
control.open()
}}>
<ButtonIcon icon={Shield} position="left" />
<ButtonIcon icon={desc.icon} position="left" />
<ButtonText style={[a.flex_1, a.text_left]}>
{desc.name}
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
{' — ' /* TODO get actual labeler */}
<Trans>Bluesky Safety</Trans>
</Text>
{desc.source && (
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
{' — '}
{sanitizeDisplayName(desc.source)}
</Text>
)}
</ButtonText>
</Button>
<ModerationDetailsDialog
control={control}
context="content"
modcause={cause}
/>
<ModerationDetailsDialog control={control} modcause={cause} />
</>
)
}
@@ -1,20 +1,20 @@
import React, {ComponentProps} from 'react'
import {StyleSheet, Pressable, View, ViewStyle, StyleProp} from 'react-native'
import {ModerationUI} from '@atproto/api'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {usePalette} from 'lib/hooks/usePalette'
import {Link} from '../Link'
import {Text} from '../text/Text'
import {addStyle} from 'lib/styles'
import {ShieldExclamation} from 'lib/icons'
import {useLingui} from '@lingui/react'
import {Trans, msg} from '@lingui/macro'
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
import {addStyle} from 'lib/styles'
import {useTheme, atoms as a} from '#/alf'
import {
ModerationDetailsDialog,
useModerationDetailsDialogControl,
} from '#/components/ModerationDetailsDialog'
} from '#/components/moderation/ModerationDetailsDialog'
import {Text} from '#/components/Typography'
// import {Link} from '#/components/Link' TODO this imposes some styles that screw things up
import {Link} from '#/view/com/util/Link'
interface Props extends ComponentProps<typeof Link> {
iconSize: number
@@ -32,12 +32,12 @@ export function PostHider({
iconStyles,
...props
}: Props) {
const pal = usePalette('default')
const t = useTheme()
const {_} = useLingui()
const [override, setOverride] = React.useState(false)
const control = useModerationDetailsDialogControl()
const blur = modui.blurs[0]
const desc = useModerationCauseDescription(blur, 'content')
const desc = useModerationCauseDescription(blur)
if (!blur) {
return (
@@ -45,7 +45,6 @@ export function PostHider({
testID={testID}
style={style}
href={href}
noFeedback
accessible={false}
{...props}>
{children}
@@ -53,7 +52,6 @@ export function PostHider({
)
}
const isMute = blur.type === 'muted'
return !override ? (
<Pressable
onPress={() => {
@@ -67,15 +65,18 @@ export function PostHider({
}
accessibilityLabel=""
style={[
styles.description,
a.flex_row,
a.align_center,
a.gap_sm,
a.py_md,
{
paddingLeft: 6,
paddingRight: 18,
},
override ? {paddingBottom: 0} : undefined,
pal.view,
t.atoms.bg,
]}>
<ModerationDetailsDialog
control={control}
context="content"
modcause={blur}
/>
<ModerationDetailsDialog control={control} modcause={blur} />
<Pressable
onPress={() => {
control.open()
@@ -85,32 +86,24 @@ export function PostHider({
accessibilityHint="">
<View
style={[
pal.viewLight,
t.atoms.bg_contrast_25,
a.align_center,
a.justify_center,
{
width: iconSize,
height: iconSize,
borderRadius: iconSize,
alignItems: 'center',
justifyContent: 'center',
},
iconStyles,
]}>
{isMute ? (
<FontAwesomeIcon
icon={['far', 'eye-slash']}
size={14}
color={pal.colors.textLight}
/>
) : (
<ShieldExclamation size={14} style={pal.textLight} />
)}
<desc.icon size="sm" fill={t.atoms.text_contrast_medium.color} />
</View>
</Pressable>
<Text type="sm" style={[{flex: 1}, pal.textLight]} numberOfLines={1}>
<Text style={[t.atoms.text_contrast_medium, a.flex_1]} numberOfLines={1}>
{desc.name}
</Text>
{!modui.noOverride && (
<Text type="sm" style={[styles.showBtn, pal.link]}>
<Text style={[{color: t.palette.primary_500}]}>
{override ? <Trans>Hide</Trans> : <Trans>Show</Trans>}
</Text>
)}
@@ -120,26 +113,14 @@ export function PostHider({
testID={testID}
style={addStyle(style, styles.child)}
href={href}
noFeedback>
accessible={false}
{...props}>
{children}
</Link>
)
}
const styles = StyleSheet.create({
description: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
paddingVertical: 10,
paddingLeft: 6,
paddingRight: 18,
marginTop: 1,
},
showBtn: {
marginLeft: 'auto',
alignSelf: 'center',
},
child: {
borderWidth: 0,
borderTopWidth: 0,
@@ -1,20 +1,18 @@
import React from 'react'
import {StyleProp, View, ViewStyle} from 'react-native'
import {ModerationCause, ModerationDecision} from '@atproto/api'
import {Text} from '../text/Text'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {getModerationCauseKey} from 'lib/moderation'
import {Trans} from '@lingui/macro'
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonText, ButtonIcon} from '#/components/Button'
import {Shield_Stroke2_Corner0_Rounded as Shield} from '#/components/icons/Shield'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash'
import {Text} from '#/components/Typography'
import {
ModerationDetailsDialog,
useModerationDetailsDialogControl,
} from '#/components/ModerationDetailsDialog'
} from '#/components/moderation/ModerationDetailsDialog'
export function ProfileHeaderAlerts({
moderation,
@@ -46,7 +44,7 @@ export function ProfileHeaderAlerts({
function ProfileInform({cause}: {cause: ModerationCause}) {
const control = useModerationDetailsDialogControl()
const desc = useModerationCauseDescription(cause, 'account')
const desc = useModerationCauseDescription(cause)
return (
<>
@@ -59,18 +57,11 @@ function ProfileInform({cause}: {cause: ModerationCause}) {
onPress={() => {
control.open()
}}>
<ButtonIcon
icon={cause.type === 'muted' ? EyeSlash : CircleInfo}
position="left"
/>{' '}
<ButtonIcon icon={desc.icon} position="left" />{' '}
<ButtonText>{desc.name}</ButtonText>
</Button>
<ModerationDetailsDialog
control={control}
context="account"
modcause={cause}
/>
<ModerationDetailsDialog control={control} modcause={cause} />
</>
)
}
@@ -78,7 +69,7 @@ function ProfileInform({cause}: {cause: ModerationCause}) {
function ProfileAlert({cause}: {cause: ModerationCause}) {
const t = useTheme()
const control = useModerationDetailsDialogControl()
const desc = useModerationCauseDescription(cause, 'account')
const desc = useModerationCauseDescription(cause)
return (
<>
@@ -91,20 +82,18 @@ function ProfileAlert({cause}: {cause: ModerationCause}) {
onPress={() => {
control.open()
}}>
<ButtonIcon icon={Shield} position="left" />
<ButtonIcon icon={desc.icon} position="left" />
<ButtonText style={[a.flex_1, a.text_left]}>
{desc.name}
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
{' — ' /* TODO get actual labeler */}
<Trans>Bluesky Safety</Trans>
</Text>
{desc.source && (
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
{' — '}
{sanitizeDisplayName(desc.source)}
</Text>
)}
</ButtonText>
</Button>
<ModerationDetailsDialog
control={control}
context="account"
modcause={cause}
/>
<ModerationDetailsDialog control={control} modcause={cause} />
</>
)
}
@@ -2,31 +2,26 @@ import React from 'react'
import {
TouchableWithoutFeedback,
StyleProp,
StyleSheet,
View,
ViewStyle,
} from 'react-native'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import {useNavigation} from '@react-navigation/native'
import {ModerationUI} from '@atproto/api'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {NavigationProp} from 'lib/routes/types'
import {Text} from '../text/Text'
import {Button} from '../forms/Button'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
import {s} from '#/lib/styles'
import {CenteredView} from '../Views'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {NavigationProp} from 'lib/routes/types'
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
import {useTheme, atoms as a} from '#/alf'
import {CenteredView} from '#/view/com/util/Views'
import {Text} from '#/components/Typography'
import {Button, ButtonText} from '#/components/Button'
import {
ModerationDetailsDialog,
useModerationDetailsDialogControl,
} from '#/components/ModerationDetailsDialog'
} from '#/components/moderation/ModerationDetailsDialog'
export function ScreenHider({
testID,
@@ -42,15 +37,14 @@ export function ScreenHider({
style?: StyleProp<ViewStyle>
containerStyle?: StyleProp<ViewStyle>
}>) {
const pal = usePalette('default')
const palInverted = usePalette('inverted')
const t = useTheme()
const {_} = useLingui()
const [override, setOverride] = React.useState(false)
const navigation = useNavigation<NavigationProp>()
const {isMobile} = useWebMediaQueries()
const control = useModerationDetailsDialogControl()
const blur = modui.blurs[0]
const desc = useModerationCauseDescription(blur, 'content')
const desc = useModerationCauseDescription(blur)
if (!blur || override) {
return (
@@ -66,25 +60,53 @@ export function ScreenHider({
)
return (
<CenteredView
style={[styles.container, pal.view, containerStyle]}
style={[
a.flex_1,
{
paddingTop: 100,
paddingBottom: 150,
},
t.atoms.bg,
containerStyle,
]}
sideBorders>
<View style={styles.iconContainer}>
<View style={[styles.icon, palInverted.view]}>
<FontAwesomeIcon
icon={isNoPwi ? ['far', 'eye-slash'] : 'exclamation'}
style={pal.textInverted as FontAwesomeIconStyle}
size={24}
/>
<View style={[a.align_center, a.mb_md]}>
<View
style={[
t.atoms.bg_contrast_975,
a.align_center,
a.justify_center,
{
borderRadius: 25,
width: 50,
height: 50,
},
]}>
<desc.icon width={24} fill={t.atoms.bg.backgroundColor} />
</View>
</View>
<Text type="title-2xl" style={[styles.title, pal.text]}>
<Text
style={[
a.text_4xl,
a.font_semibold,
a.text_center,
a.mb_md,
t.atoms.text,
]}>
{isNoPwi ? (
<Trans>Sign-in Required</Trans>
) : (
<Trans>Content Warning</Trans>
)}
</Text>
<Text type="2xl" style={[styles.description, pal.textLight]}>
<Text
style={[
a.text_lg,
a.mb_md,
a.px_lg,
a.text_center,
t.atoms.text_contrast_medium,
]}>
{isNoPwi ? (
<Trans>
This account has requested that users sign in to view their profile.
@@ -92,7 +114,7 @@ export function ScreenHider({
) : (
<>
<Trans>This {screenDescription} has been flagged:</Trans>
<Text type="2xl-medium" style={[pal.text, s.ml5]}>
<Text style={[a.text_lg, a.font_semibold, t.atoms.text, a.ml_xs]}>
{desc.name}.{' '}
</Text>
<TouchableWithoutFeedback
@@ -102,87 +124,48 @@ export function ScreenHider({
accessibilityRole="button"
accessibilityLabel={_(msg`Learn more about this warning`)}
accessibilityHint="">
<Text type="2xl" style={pal.link}>
<Text style={[a.text_lg, {color: t.palette.primary_500}]}>
<Trans>Learn More</Trans>
</Text>
</TouchableWithoutFeedback>
<ModerationDetailsDialog
control={control}
context="account"
modcause={blur}
/>
<ModerationDetailsDialog control={control} modcause={blur} />
</>
)}{' '}
</Text>
{isMobile && <View style={styles.spacer} />}
<View style={styles.btnContainer}>
{isMobile && <View style={a.flex_1} />}
<View style={[a.flex_row, a.justify_center, a.my_md, a.gap_md]}>
<Button
type="inverted"
variant="solid"
color="primary"
size="large"
style={[a.rounded_full]}
label={_(msg`Go back`)}
onPress={() => {
if (navigation.canGoBack()) {
navigation.goBack()
} else {
navigation.navigate('Home')
}
}}
style={styles.btn}>
<Text type="button-lg" style={pal.textInverted}>
}}>
<ButtonText>
<Trans>Go back</Trans>
</Text>
</ButtonText>
</Button>
{!modui.noOverride && (
<Button
type="default"
onPress={() => setOverride(v => !v)}
style={styles.btn}>
<Text type="button-lg" style={pal.text}>
variant="solid"
color="secondary"
size="large"
style={[a.rounded_full]}
label={_(msg`Show anyway`)}
onPress={() => setOverride(v => !v)}>
<ButtonText>
<Trans>Show anyway</Trans>
</Text>
</ButtonText>
</Button>
)}
</View>
</CenteredView>
)
}
const styles = StyleSheet.create({
spacer: {
flex: 1,
},
container: {
flex: 1,
paddingTop: 100,
paddingBottom: 150,
},
iconContainer: {
alignItems: 'center',
marginBottom: 10,
},
icon: {
borderRadius: 25,
width: 50,
height: 50,
alignItems: 'center',
justifyContent: 'center',
},
title: {
textAlign: 'center',
marginBottom: 10,
},
description: {
marginBottom: 10,
paddingHorizontal: 20,
textAlign: 'center',
},
btnContainer: {
flexDirection: 'row',
justifyContent: 'center',
marginVertical: 10,
gap: 10,
},
btn: {
paddingHorizontal: 20,
paddingVertical: 14,
},
})
@@ -0,0 +1,70 @@
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useMemo} from 'react'
export type GlobalLabelStrings = Record<
string,
{
name: string
description: string
}
>
export function useGlobalLabelStrings(): GlobalLabelStrings {
const {_} = useLingui()
return useMemo(
() => ({
'!hide': {
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: _(
msg`This content has received a general warning from moderators.`,
),
},
'!no-unauthenticated': {
name: _(msg`Sign-in Required`),
description: _(
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.`),
},
sexual: {
name: _(msg`Sexually Suggestive`),
description: _(msg`Does not include nudity.`),
},
nudity: {
name: _(msg`Nudity`),
description: _(msg`Including non-sexual and artistic.`),
},
gore: {
name: _(msg`Violent / Bloody`),
description: _(msg`Gore, self-harm, torture`),
},
}),
[_],
)
}
+99
View File
@@ -0,0 +1,99 @@
import {
ComAtprotoLabelDefs,
AppBskyLabelerDefs,
LABELS,
interpretLabelValueDefinition,
InterprettedLabelValueDefinition,
} from '@atproto/api'
import {useLingui} from '@lingui/react'
import * as bcp47Match from 'bcp-47-match'
import {
useGlobalLabelStrings,
GlobalLabelStrings,
} from '#/lib/moderation/useGlobalLabelStrings'
import {useLabelDefinitions} from '#/state/queries/preferences'
export interface LabelInfo {
label: ComAtprotoLabelDefs.Label
def: InterprettedLabelValueDefinition
strings: ComAtprotoLabelDefs.LabelValueDefinitionStrings
labeler: AppBskyLabelerDefs.LabelerViewDetailed | undefined
}
export function useLabelInfo(label: ComAtprotoLabelDefs.Label): LabelInfo {
const {i18n} = useLingui()
const globalLabelStrings = useGlobalLabelStrings()
const {labelDefs, labelers} = useLabelDefinitions()
const def = getDefinition(labelDefs, label)
return {
label,
def,
strings: getLabelStrings(i18n.locale, globalLabelStrings, def),
labeler: labelers.find(labeler => label.src === labeler.creator.did),
}
}
export function getDefinition(
labelDefs: Record<string, InterprettedLabelValueDefinition[]>,
label: ComAtprotoLabelDefs.Label,
): InterprettedLabelValueDefinition {
// check local definitions
const customDef =
!label.val.startsWith('!') &&
labelDefs[label.src].find(
def => def.identifier === label.val && def.definedBy === label.src,
)
if (customDef) {
return customDef
}
// check global definitions
const globalDef = LABELS[label.val as keyof typeof LABELS]
if (globalDef) {
return globalDef
}
// fallback to a noop definition
return interpretLabelValueDefinition(
{
identifier: label.val,
severity: 'none',
blurs: 'none',
locales: [],
},
label.src,
)
}
export function getLabelStrings(
locale: string,
globalLabelStrings: GlobalLabelStrings,
def: InterprettedLabelValueDefinition,
): ComAtprotoLabelDefs.LabelValueDefinitionStrings {
if (!def.definedBy) {
// global definition, look up strings
if (def.identifier in globalLabelStrings) {
return globalLabelStrings[
def.identifier
] as ComAtprotoLabelDefs.LabelValueDefinitionStrings
}
} else {
// try to find locale match in the definition's strings
const localeMatch = def.locales.find(
strings => bcp47Match.basicFilter(locale, strings.lang).length > 0,
)
if (localeMatch) {
return localeMatch
}
// fall back to the zero item if no match
if (def.locales[0]) {
return def.locales[0]
}
}
return {
lang: locale,
name: def.identifier,
description: `Labeled "${def.identifier}"`,
}
}
-203
View File
@@ -1,203 +0,0 @@
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useMemo} from 'react'
export type LabelStrings = Record<
string,
{
general: {name: string; description: string}
account: {name: string; description: string}
content: {name: string; description: string}
}
>
export function useLabelStrings(): LabelStrings {
const {_} = useLingui()
return useMemo(
() => ({
'!hide': {
general: {
name: _(msg`Moderator Hide`),
description: _(msg`Moderator has chosen to hide the content.`),
},
account: {
name: _(msg`Content Blocked`),
description: _(msg`This account has been hidden by the moderators.`),
},
content: {
name: _(msg`Content Blocked`),
description: _(msg`This content has been hidden by the moderators.`),
},
},
'!no-promote': {
general: {
name: _(msg`Moderator Filter`),
description: _(
msg`Moderator has chosen to filter the content from feeds.`,
),
},
account: {
name: _(msg`N/A`),
description: _(msg`N/A`),
},
content: {
name: _(msg`N/A`),
description: _(msg`N/A`),
},
},
'!warn': {
general: {
name: _(msg`Moderator Warn`),
description: _(
msg`Moderator has chosen to set a general warning on the content.`,
),
},
account: {
name: _(msg`Content Warning`),
description: _(
msg`This account has received a general warning from moderators.`,
),
},
content: {
name: _(msg`Content Warning`),
description: _(
msg`This content has received a general warning from moderators.`,
),
},
},
'!no-unauthenticated': {
general: {
name: _(msg`Sign-in Required`),
description: _(
msg`This user has requested that their account only be shown to signed-in users.`,
),
},
account: {
name: _(msg`Sign-in Required`),
description: _(
msg`This user has requested that their account only be shown to signed-in users.`,
),
},
content: {
name: _(msg`Sign-in Required`),
description: _(
msg`This user has requested that their content only be shown to signed-in users.`,
),
},
},
'dmca-violation': {
general: {
name: _(msg`Copyright Violation`),
description: _(
msg`The content has received a DMCA takedown request.`,
),
},
account: {
name: _(msg`Copyright Violation`),
description: _(
msg`This account has received a DMCA takedown request. It will be restored if the concerns can be resolved.`,
),
},
content: {
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: {
general: {
name: _(msg`Doxxing`),
description: _(
msg`Information that reveals private information about someone which has been shared without the consent of the subject.`,
),
},
account: {
name: _(msg`Doxxing`),
description: _(
msg`This account has been reported to publish private information about someone without their consent. This report is currently under review.`,
),
},
content: {
name: _(msg`Doxxing`),
description: _(
msg`This content has been reported to include private information about someone without their consent.`,
),
},
},
porn: {
general: {
name: _(msg`Pornography`),
description: _(msg`Explicit sexual images.`),
},
account: {
name: _(msg`Adult Content`),
description: _(
msg`This account contains imagery of full-frontal nudity or explicit sexual activity.`,
),
},
content: {
name: _(msg`Adult Content`),
description: _(
msg`This content contains imagery of full-frontal nudity or explicit sexual activity.`,
),
},
},
sexual: {
general: {
name: _(msg`Sexually Suggestive`),
description: _(msg`Does not include nudity.`),
},
account: {
name: _(msg`Suggestive Content`),
description: _(
msg`This account contains imagery which is sexually suggestive. Common examples include selfies in underwear or in partial undress.`,
),
},
content: {
name: _(msg`Suggestive Content`),
description: _(
msg`This content contains imagery which is sexually suggestive. Common examples include selfies in underwear or in partial undress.`,
),
},
},
nudity: {
general: {
name: _(msg`Nudity`),
description: _(msg`Including non-sexual and artistic.`),
},
account: {
name: _(msg`Adult Content`),
description: _(
msg`This account contains imagery which portrays nudity in a non-sexual or artistic setting.`,
),
},
content: {
name: _(msg`Adult Content`),
description: _(
msg`This content contains imagery which portrays nudity in a non-sexual or artistic setting.`,
),
},
},
gore: {
general: {
name: _(msg`Violent / Bloody`),
description: _(msg`Gore, self-harm, torture`),
},
account: {
name: _(msg`Graphic Imagery (Gore)`),
description: _(
msg`This account contains shocking images involving blood or visible wounds.`,
),
},
content: {
name: _(msg`Graphic Imagery (Gore)`),
description: _(
msg`This content contains shocking images involving blood or visible wounds.`,
),
},
},
}),
[_],
)
}
@@ -1,21 +1,31 @@
import {ModerationCause, LABELS} from '@atproto/api'
import {ModerationCause} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useLabelStrings} from './useLabelStrings'
import {useGlobalLabelStrings} from './useGlobalLabelStrings'
import {useLabelDefinitions} from '#/state/queries/preferences'
import {getDefinition, getLabelStrings} from './useLabelInfo'
import {TriangleExclamation_Stroke2_Corner2_Rounded as TriangleExclamation} from '#/components/icons/TriangleExclamation'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash'
import {CircleBanSign_Stroke2_Corner0_Rounded as CircleBanSign} from '#/components/icons/CircleBanSign'
export interface ModerationCauseDescription {
icon: any
name: string
description: string
source?: string
}
export function useModerationCauseDescription(
cause: ModerationCause | undefined,
context: 'account' | 'content',
): ModerationCauseDescription {
const {_} = useLingui()
const labelStrings = useLabelStrings()
const {_, i18n} = useLingui()
const globalLabelStrings = useGlobalLabelStrings()
const {labelDefs, labelers} = useLabelDefinitions()
if (!cause) {
return {
icon: TriangleExclamation,
name: _(msg`Content Warning`),
description: _(
msg`Moderator has chosen to set a general warning on the content.`,
@@ -25,6 +35,7 @@ export function useModerationCauseDescription(
if (cause.type === 'blocking') {
if (cause.source.type === 'list') {
return {
icon: CircleBanSign,
name: _(msg`User Blocked by "${cause.source.list.name}"`),
description: _(
msg`You have blocked this user. You cannot view their content.`,
@@ -32,6 +43,7 @@ export function useModerationCauseDescription(
}
} else {
return {
icon: CircleBanSign,
name: _(msg`User Blocked`),
description: _(
msg`You have blocked this user. You cannot view their content.`,
@@ -41,6 +53,7 @@ export function useModerationCauseDescription(
}
if (cause.type === 'blocked-by') {
return {
icon: CircleBanSign,
name: _(msg`User Blocking You`),
description: _(
msg`This user has blocked you. You cannot view their content.`,
@@ -49,6 +62,7 @@ export function useModerationCauseDescription(
}
if (cause.type === 'block-other') {
return {
icon: CircleBanSign,
name: _(msg`Content Not Available`),
description: _(
msg`This content is not available because one of the users involved has blocked the other.`,
@@ -58,11 +72,13 @@ export function useModerationCauseDescription(
if (cause.type === 'muted') {
if (cause.source.type === 'list') {
return {
icon: EyeSlash,
name: _(msg`Muted by "${cause.source.list.name}"`),
description: _(msg`You have muted this user`),
}
} else {
return {
icon: EyeSlash,
name: _(msg`Muted User`),
description: _(msg`You have muted this user`),
}
@@ -71,30 +87,30 @@ export function useModerationCauseDescription(
// @ts-ignore Temporary extension to the moderation system -prf
if (cause.type === 'hidden') {
return {
icon: EyeSlash,
name: _(msg`Post Hidden by You`),
description: _(msg`You have hidden this post`),
}
}
if (cause.type === 'label') {
if (cause.labelDef.identifier in labelStrings) {
const strings =
labelStrings[cause.labelDef.identifier as keyof typeof LABELS]
return {
name:
context === 'account' ? strings.account.name : strings.content.name,
description:
context === 'account'
? strings.account.description
: strings.content.description,
}
}
const def = getDefinition(labelDefs, cause.label)
const strings = getLabelStrings(i18n.locale, globalLabelStrings, def)
const labeler = labelers.find(l => l.creator.did === cause.label.src)
return {
name: cause.labelDef.identifier,
description: _(msg`Labeled ${cause.labelDef.identifier}`),
icon:
def.identifier === '!no-unauthenticated'
? EyeSlash
: def.severity === 'alert'
? TriangleExclamation
: CircleInfo,
name: strings.name,
description: strings.description,
source: labeler?.creator.displayName || labeler?.creator.handle,
}
}
// should never happen
return {
icon: CircleInfo,
name: '',
description: ``,
}
+18 -35
View File
@@ -82,17 +82,17 @@ export function ModerationScreen(
data: preferences,
} = usePreferencesQuery()
const {
isLoading: isModServicesLoading,
data: modservices,
error: modservicesError,
isLoading: isLabelersLoading,
data: labelers,
error: labelersError,
} = useLabelersDetailedInfoQuery({
dids: preferences ? preferences.moderationPrefs.mods.map(m => m.did) : [],
})
const {gtMobile} = useBreakpoints()
const {height} = useSafeAreaFrame()
const isLoading = isPreferencesLoading || isModServicesLoading
const error = preferencesError || modservicesError
const isLoading = isPreferencesLoading || isLabelersLoading
const error = preferencesError || labelersError
return (
<CenteredView
@@ -109,7 +109,7 @@ export function ModerationScreen(
<View style={[a.w_full, a.align_center, a.pt_2xl]}>
<Loader size="xl" fill={t.atoms.text.color} />
</View>
) : error || !(preferences && modservices) ? (
) : error || !(preferences && labelers) ? (
<ErrorState
error={
preferencesError?.toString() ||
@@ -117,10 +117,7 @@ export function ModerationScreen(
}
/>
) : (
<ModerationScreenInner
preferences={preferences}
modservices={modservices}
/>
<ModerationScreenInner preferences={preferences} labelers={labelers} />
)}
</CenteredView>
)
@@ -128,10 +125,10 @@ export function ModerationScreen(
export function ModerationScreenInner({
preferences,
modservices,
labelers,
}: {
preferences: UsePreferencesQueryResponse
modservices: AppBskyLabelerDefs.LabelerViewDetailed[]
labelers: AppBskyLabelerDefs.LabelerViewDetailed[]
}) {
const t = useTheme()
const setMinimalShellMode = useSetMinimalShellMode()
@@ -297,25 +294,13 @@ export function ModerationScreenInner({
{adultContentEnabled && (
<>
<Divider />
<SimpleModerationLabelPref
labelValueDefinition={LABELS.porn}
labelerDid={undefined}
/>
<SimpleModerationLabelPref labelValueDefinition={LABELS.porn} />
<Divider />
<SimpleModerationLabelPref
labelValueDefinition={LABELS.sexual}
labelerDid={undefined}
/>
<SimpleModerationLabelPref labelValueDefinition={LABELS.sexual} />
<Divider />
<SimpleModerationLabelPref
labelValueDefinition={LABELS.nudity}
labelerDid={undefined}
/>
<SimpleModerationLabelPref labelValueDefinition={LABELS.nudity} />
<Divider />
<SimpleModerationLabelPref
labelValueDefinition={LABELS.gore}
labelerDid={undefined}
/>
<SimpleModerationLabelPref labelValueDefinition={LABELS.gore} />
</>
)}
</View>
@@ -332,7 +317,7 @@ export function ModerationScreenInner({
</Text>
<View style={[a.rounded_sm, t.atoms.bg_contrast_25]}>
<ModerationServiceCard.Link
modservice={{
labeler={{
uri: '',
cid: '',
policies: {
@@ -357,13 +342,11 @@ export function ModerationScreenInner({
/>
</ModerationServiceCard.Card.Outer>
</ModerationServiceCard.Link>
{modservices.map(mod => {
{labelers.map(mod => {
return (
<>
<React.Fragment key={mod.creator.did}>
<Divider />
<ModerationServiceCard.Link
modservice={mod}
key={mod.creator.did}>
<ModerationServiceCard.Link labeler={mod}>
<ModerationServiceCard.Card.Outer>
<ModerationServiceCard.Card.Avatar
avatar={mod.creator.avatar}
@@ -378,7 +361,7 @@ export function ModerationScreenInner({
/>
</ModerationServiceCard.Card.Outer>
</ModerationServiceCard.Link>
</>
</React.Fragment>
)
})}
</View>
+2 -2
View File
@@ -13,12 +13,12 @@ import {Shadow} from '#/state/cache/types'
import {useLightboxControls, ProfileImageLightbox} from '#/state/lightbox'
import {atoms as a, useTheme} from '#/alf'
import {LabelsOnMe} from 'view/com/util/moderation/LabelsOnMe'
import {LabelsOnMe} from '#/components/moderation/LabelsOnMe'
import {BlurView} from 'view/com/util/BlurView'
import {LoadingPlaceholder} from 'view/com/util/LoadingPlaceholder'
import {UserAvatar} from 'view/com/util/UserAvatar'
import {UserBanner} from 'view/com/util/UserBanner'
import {ProfileHeaderAlerts} from 'view/com/util/moderation/ProfileHeaderAlerts'
import {ProfileHeaderAlerts} from '#/components/moderation/ProfileHeaderAlerts'
interface Props {
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>
+7 -3
View File
@@ -8,7 +8,7 @@ import {
import {track} from '#/lib/analytics/analytics'
import {getAge} from '#/lib/strings/time'
import {getAgent} from '#/state/session'
import {getAgent, useSession} from '#/state/session'
import {
ConfigurableLabelGroup,
UsePreferencesQueryResponse,
@@ -21,6 +21,7 @@ import {
} from '#/state/queries/preferences/const'
import {STALE} from '#/state/queries'
import {useHiddenPosts} from '#/state/preferences/hidden-posts'
import {useLabelDefinitions} from '#/state/queries/preferences/moderation'
export * from '#/state/queries/preferences/types'
export * from '#/state/queries/preferences/moderation'
@@ -74,7 +75,9 @@ export const moderationOptsOverrideContext = createContext<
export function useModerationOpts() {
const override = useContext(moderationOptsOverrideContext)
const {currentAccount} = useSession()
const prefs = usePreferencesQuery()
const {labelDefs} = useLabelDefinitions()
const hiddenPosts = useHiddenPosts()
const opts = useMemo<ModerationOpts | undefined>(() => {
if (override) {
@@ -85,13 +88,14 @@ export function useModerationOpts() {
}
const moderationPrefs = prefs.data.moderationPrefs
return {
userDid: '', // TODO
userDid: currentAccount?.did,
prefs: {
...moderationPrefs,
hiddenPosts,
},
labelDefs,
}
}, [override, prefs.data, hiddenPosts])
}, [override, currentAccount, labelDefs, prefs.data, hiddenPosts])
return opts
}
+41 -5
View File
@@ -1,11 +1,26 @@
import {ComAtprotoLabelDefs, DEFAULT_LABEL_SETTINGS} from '@atproto/api'
import {
ComAtprotoLabelDefs,
AppBskyLabelerDefs,
DEFAULT_LABEL_SETTINGS,
LABELS,
BSKY_LABELER_DID,
interpretLabelValueDefinition,
interpretLabelValueDefinitions,
InterprettedLabelValueDefinition,
} from '@atproto/api'
import {useLingui} from '@lingui/react'
import * as bcp47Match from 'bcp-47-match'
import {
LabelGroup,
ConfigurableLabelGroup,
} from '#/state/queries/preferences/types'
export type Label = ComAtprotoLabelDefs.Label
import {usePreferencesQuery} from './index'
import {useLabelersDetailedInfoQuery} from '../labeler'
import {
useGlobalLabelStrings,
GlobalLabelStrings,
} from '#/lib/moderation/useGlobalLabelStrings'
export type LabelGroupConfig = {
id: LabelGroup
@@ -18,8 +33,6 @@ export type LabelGroupConfig = {
/**
* More strict than our default settings for logged in users.
*
* TODO(pwi)
*/
export const DEFAULT_LOGGED_OUT_LABEL_PREFERENCES: typeof DEFAULT_LABEL_SETTINGS =
Object.fromEntries(
@@ -84,3 +97,26 @@ export const CONFIGURABLE_LABEL_GROUPS: Record<
values: ['impersonation'],
},
}
export function useMyLabelers() {
const prefs = usePreferencesQuery()
const dids = prefs.data?.moderationPrefs.mods.map(m => m.did) || []
if (!dids.includes(BSKY_LABELER_DID)) {
dids.push(BSKY_LABELER_DID)
}
const labelers = useLabelersDetailedInfoQuery({dids})
return labelers.data || []
}
export function useLabelDefinitions() {
const labelers = useMyLabelers()
return {
labelDefs: Object.fromEntries(
labelers.map(labeler => [
labeler.creator.did,
interpretLabelValueDefinitions(labeler),
]),
),
labelers,
}
}
+4 -4
View File
@@ -23,10 +23,10 @@ import {getTranslatorLink, isPostInLanguage} from '../../../locale/helpers'
import {PostMeta} from '../util/PostMeta'
import {PostEmbeds} from '../util/post-embeds'
import {PostCtrls} from '../util/post-ctrls/PostCtrls'
import {PostHider} from '../util/moderation/PostHider'
import {ContentHider} from '../util/moderation/ContentHider'
import {PostAlerts} from '../util/moderation/PostAlerts'
import {LabelsOnMyPost} from '../util/moderation/LabelsOnMe'
import {PostHider} from '../../../components/moderation/PostHider'
import {ContentHider} from '../../../components/moderation/ContentHider'
import {PostAlerts} from '../../../components/moderation/PostAlerts'
import {LabelsOnMyPost} from '../../../components/moderation/LabelsOnMe'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {usePalette} from 'lib/hooks/usePalette'
import {formatCount} from '../util/numeric/format'
+3 -3
View File
@@ -14,9 +14,9 @@ import {UserInfoText} from '../util/UserInfoText'
import {PostMeta} from '../util/PostMeta'
import {PostEmbeds} from '../util/post-embeds'
import {PostCtrls} from '../util/post-ctrls/PostCtrls'
import {ContentHider} from '../util/moderation/ContentHider'
import {PostAlerts} from '../util/moderation/PostAlerts'
import {LabelsOnMyPost} from '../util/moderation/LabelsOnMe'
import {ContentHider} from '../../../components/moderation/ContentHider'
import {PostAlerts} from '../../../components/moderation/PostAlerts'
import {LabelsOnMyPost} from '../../../components/moderation/LabelsOnMe'
import {Text} from '../util/text/Text'
import {RichText} from '#/components/RichText'
import {PreviewableUserAvatar} from '../util/UserAvatar'
+3 -3
View File
@@ -18,9 +18,9 @@ import {UserInfoText} from '../util/UserInfoText'
import {PostMeta} from '../util/PostMeta'
import {PostCtrls} from '../util/post-ctrls/PostCtrls'
import {PostEmbeds} from '../util/post-embeds'
import {ContentHider} from '../util/moderation/ContentHider'
import {PostAlerts} from '../util/moderation/PostAlerts'
import {LabelsOnMyPost} from '../util/moderation/LabelsOnMe'
import {ContentHider} from '#/components/moderation/ContentHider'
import {PostAlerts} from '../../../components/moderation/PostAlerts'
import {LabelsOnMyPost} from '../../../components/moderation/LabelsOnMe'
import {RichText} from '#/components/RichText'
import {PreviewableUserAvatar} from '../util/UserAvatar'
import {s} from 'lib/styles'
@@ -1,136 +0,0 @@
import React from 'react'
import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
import {ModerationUI} from '@atproto/api'
import {useLingui} from '@lingui/react'
import {msg, Trans} from '@lingui/macro'
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
import {isJustAMute} from '#/lib/moderation'
import {atoms as a, useTheme, useBreakpoints} from '#/alf'
import {Button, ButtonText, ButtonIcon} from '#/components/Button'
import {Shield_Stroke2_Corner0_Rounded as Shield} from '#/components/icons/Shield'
import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash'
import {Text} from '#/components/Typography'
import {
ModerationDetailsDialog,
useModerationDetailsDialogControl,
} from '#/components/ModerationDetailsDialog'
export function ContentHider({
testID,
modui,
ignoreMute,
style,
childContainerStyle,
children,
}: React.PropsWithChildren<{
testID?: string
modui: ModerationUI | undefined
ignoreMute?: boolean
style?: StyleProp<ViewStyle>
childContainerStyle?: StyleProp<ViewStyle>
}>) {
const t = useTheme()
const {_} = useLingui()
const [override, setOverride] = React.useState(false)
const {gtMobile} = useBreakpoints()
const control = useModerationDetailsDialogControl()
const blur = modui?.blurs[0]
const desc = useModerationCauseDescription(blur, 'content')
if (!blur || (ignoreMute && isJustAMute(modui))) {
return (
<View testID={testID} style={[styles.outer, style]}>
{children}
</View>
)
}
return (
<View testID={testID} style={[a.overflow_hidden, style]}>
<ModerationDetailsDialog
control={control}
context="content"
modcause={blur}
/>
<View style={[a.flex_col, a.gap_xs]}>
<Button
variant="solid"
color="secondary"
size={gtMobile ? 'large' : 'small'}
shape="default"
onPress={() => {
if (!modui.noOverride) {
setOverride(v => !v)
} else {
control.open()
}
}}
label={desc.name}
accessibilityHint={
override ? _(msg`Hide the content`) : _(msg`Show the content`)
}>
<ButtonIcon
icon={blur.type === 'muted' ? EyeSlash : Shield}
position="left"
/>{' '}
<ButtonText style={[a.flex_1, a.text_left]}>{desc.name}</ButtonText>
{!modui.noOverride && (
<ButtonText>
{override ? <Trans>Hide</Trans> : <Trans>Show</Trans>}
</ButtonText>
)}
</Button>
{blur.type === 'label' && !override && (
<Button
variant="ghost"
size="tiny"
onPress={() => {
control.open()
}}
label={_(msg`Learn more`)}>
<ButtonText
style={[
a.flex_1,
a.text_sm,
a.font_normal,
t.atoms.text_contrast_medium,
a.text_left,
]}>
{/* TODO get actual labeler */}
<Trans>
Labeled by Bluesky Safety.{' '}
<Text style={[{color: t.palette.primary_500}, a.text_sm]}>
Learn more.
</Text>
</Trans>
</ButtonText>
</Button>
)}
</View>
{override && <View style={childContainerStyle}>{children}</View>}
</View>
)
}
const styles = StyleSheet.create({
outer: {
overflow: 'hidden',
},
cover: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
borderRadius: 8,
marginTop: 4,
paddingVertical: 14,
paddingLeft: 14,
paddingRight: 18,
},
showBtn: {
marginLeft: 'auto',
alignSelf: 'center',
},
})
+2 -2
View File
@@ -18,12 +18,12 @@ import {Text} from '../text/Text'
import {usePalette} from 'lib/hooks/usePalette'
import {ComposerOptsQuote} from 'state/shell/composer'
import {PostEmbeds} from '.'
import {PostAlerts} from '../moderation/PostAlerts'
import {PostAlerts} from '../../../../components/moderation/PostAlerts'
import {makeProfileLink} from 'lib/routes/links'
import {InfoCircleIcon} from 'lib/icons'
import {Trans} from '@lingui/macro'
import {useModerationOpts} from '#/state/queries/preferences'
import {ContentHider} from '../moderation/ContentHider'
import {ContentHider} from '../../../../components/moderation/ContentHider'
import {RichText} from '#/components/RichText'
import {atoms as a} from '#/alf'
+1 -1
View File
@@ -26,7 +26,7 @@ import {MaybeQuoteEmbed} from './QuoteEmbed'
import {AutoSizedImage} from '../images/AutoSizedImage'
import {ListEmbed} from './ListEmbed'
import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard'
import {ContentHider} from '../moderation/ContentHider'
import {ContentHider} from '../../../../components/moderation/ContentHider'
import {isNative} from '#/platform/detection'
import {shareUrl} from '#/lib/sharing'
+4 -4
View File
@@ -28,7 +28,7 @@ import {
import {atoms as a, useTheme} from '#/alf'
import {CenteredView, ScrollView} from '#/view/com/util/Views'
import {H1, H3, P, Text} from '#/components/Typography'
import {useLabelStrings} from '#/lib/moderation/useLabelStrings'
import {useGlobalLabelStrings} from '#/lib/moderation/useGlobalLabelStrings'
import * as Toggle from '#/components/forms/Toggle'
import * as ToggleButton from '#/components/forms/ToggleButton'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
@@ -37,7 +37,7 @@ import {
ChevronBottom_Stroke2_Corner0_Rounded as ChevronBottom,
ChevronTop_Stroke2_Corner0_Rounded as ChevronTop,
} from '#/components/icons/Chevron'
import {ScreenHider} from '../com/util/moderation/ScreenHider'
import {ScreenHider} from '../../components/moderation/ScreenHider'
import {ProfileHeader} from '#/screens/Profile/Header'
import {ProfileCard} from '../com/profile/ProfileCard'
import {FeedItem} from '../com/posts/FeedItem'
@@ -73,7 +73,7 @@ export const DebugModScreen = ({}: NativeStackScreenProps<
],
})
const [view, setView] = React.useState<string[]>(['post'])
const labelStrings = useLabelStrings()
const labelStrings = useGlobalLabelStrings()
const {currentAccount} = useSession()
const isTargetMe =
@@ -309,7 +309,7 @@ export const DebugModScreen = ({}: NativeStackScreenProps<
<Toggle.Item
key={labelValue}
name={labelValue}
label={labelStrings[labelValue].general.name}
label={labelStrings[labelValue].name}
disabled={disabled}
style={disabled ? {opacity: 0.5} : undefined}>
<Toggle.Radio />
+1 -1
View File
@@ -12,7 +12,7 @@ import {useLingui} from '@lingui/react'
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
import {CenteredView} from '../com/util/Views'
import {ListRef} from '../com/util/List'
import {ScreenHider} from 'view/com/util/moderation/ScreenHider'
import {ScreenHider} from '#/components/moderation/ScreenHider'
import {ProfileLists} from '../com/lists/ProfileLists'
import {ProfileFeedgens} from '../com/feeds/ProfileFeedgens'
import {PagerWithHeader} from 'view/com/pager/PagerWithHeader'