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 {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLabelStrings} from '#/lib/moderation/useLabelStrings' import {useGlobalLabelStrings} from '#/lib/moderation/useGlobalLabelStrings'
import { import {
usePreferencesQuery, usePreferencesQuery,
usePreferencesSetContentLabelMutation, usePreferencesSetContentLabelMutation,
@@ -30,10 +30,10 @@ export function SimpleModerationLabelPref({
const savedPref = preferences?.moderationPrefs.labels[identifier] const savedPref = preferences?.moderationPrefs.labels[identifier]
const pref = variables?.visibility ?? savedPref ?? 'warn' const pref = variables?.visibility ?? savedPref ?? 'warn'
const allLabelStrings = useLabelStrings() const allLabelStrings = useGlobalLabelStrings()
const labelStrings = const labelStrings =
labelValueDefinition.identifier in allLabelStrings labelValueDefinition.identifier in allLabelStrings
? allLabelStrings[labelValueDefinition.identifier].general ? allLabelStrings[labelValueDefinition.identifier]
: { : {
name: labelValueDefinition.identifier, name: labelValueDefinition.identifier,
description: `Labeled "${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 {useLingui} from '@lingui/react'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLabelStrings} from '#/lib/moderation/useLabelStrings' import {useGlobalLabelStrings} from '#/lib/moderation/useGlobalLabelStrings'
import { import {
useLabelBehaviorDescription, useLabelBehaviorDescription,
useLabelLongBehaviorDescription, useLabelLongBehaviorDescription,
@@ -58,11 +58,11 @@ export function ModerationLabelPref({
'ignore', 'ignore',
) )
const allLabelStrings = useLabelStrings() const allLabelStrings = useGlobalLabelStrings()
const labelStrings = labelValueDefinition.locales[0] // TODO look up locale const labelStrings = labelValueDefinition.locales[0] // TODO look up locale
? labelValueDefinition.locales[0] ? labelValueDefinition.locales[0]
: labelValueDefinition.identifier in allLabelStrings : labelValueDefinition.identifier in allLabelStrings
? allLabelStrings[labelValueDefinition.identifier].general ? allLabelStrings[labelValueDefinition.identifier]
: { : {
name: labelValueDefinition.identifier, name: labelValueDefinition.identifier,
description: `Labeled "${labelValueDefinition.identifier}"`, description: `Labeled "${labelValueDefinition.identifier}"`,
@@ -11,12 +11,12 @@ import {useLabelerInfoQuery} from '#/state/queries/labeler'
export * as Card from '#/components/ModerationServiceCard/Card' export * as Card from '#/components/ModerationServiceCard/Card'
type ModerationServiceProps = { type ModerationServiceProps = {
modservice: AppBskyLabelerDefs.LabelerViewDetailed labeler: AppBskyLabelerDefs.LabelerViewDetailed
} }
export function Link({ export function Link({
children, children,
modservice, labeler,
}: ModerationServiceProps & Pick<LinkProps, 'children'>) { }: ModerationServiceProps & Pick<LinkProps, 'children'>) {
const {_} = useLingui() const {_} = useLingui()
@@ -25,11 +25,11 @@ export function Link({
to={{ to={{
screen: 'Profile', screen: 'Profile',
params: { params: {
name: modservice.creator.handle, name: labeler.creator.handle,
}, },
}} }}
label={_( label={_(
msg`View the moderation service provided by @${modservice.creator.handle}`, msg`View the moderation service provided by @${labeler.creator.handle}`,
)}> )}>
{children} {children}
</InternalLink> </InternalLink>
@@ -54,7 +54,7 @@ export function Loader({
loading?: React.ComponentType<{}> loading?: React.ComponentType<{}>
error?: React.ComponentType<{error: string}> error?: React.ComponentType<{error: string}>
component: React.ComponentType<{ component: React.ComponentType<{
modservice: AppBskyLabelerDefs.LabelerViewDetailed labeler: AppBskyLabelerDefs.LabelerViewDetailed
}> }>
}) { }) {
const {isLoading, data, error} = useLabelerInfoQuery({did}) const {isLoading, data, error} = useLabelerInfoQuery({did})
@@ -68,6 +68,6 @@ export function Loader({
<ErrorComponent error={error?.message || 'Unknown error'} /> <ErrorComponent error={error?.message || 'Unknown error'} />
) : null ) : 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 t = useTheme()
const ctx = Toggle.useItemContext() const ctx = Toggle.useItemContext()
@@ -260,7 +260,7 @@ function SubmitView({
key={labeler.creator.did} key={labeler.creator.did}
name={labeler.creator.did} name={labeler.creator.did}
label={title}> label={title}>
<ModServiceToggle title={title} /> <LabelerToggle title={title} />
</Toggle.Item> </Toggle.Item>
) )
})} })}
@@ -477,21 +477,21 @@ function ReportDialogInner(props: ReportDialogProps) {
data: preferences, data: preferences,
} = usePreferencesQuery() } = usePreferencesQuery()
const { const {
isLoading: isModServicesLoading, isLoading: isLabelersLoading,
data: modservices, data: labelers,
error: modservicesError, error: labelersError,
} = useLabelersDetailedInfoQuery({ } = useLabelersDetailedInfoQuery({
dids: preferences ? preferences.moderationOpts.mods.map(m => m.did) : [], dids: preferences ? preferences.moderationPrefs.mods.map(m => m.did) : [],
}) })
const isLoading = isPreferencesLoading || isModServicesLoading const isLoading = isPreferencesLoading || isLabelersLoading
const error = preferencesError || modservicesError const error = preferencesError || labelersError
const [fakeLoading, setFakeLoading] = React.useState(isLoading) const [fakeLoading, setFakeLoading] = React.useState(isLoading)
const labelGroupToLabelerMap = React.useMemo(() => { const labelGroupToLabelerMap = React.useMemo(() => {
if (!modservices) return {} if (!labelers) return {}
return getLabelGroupToLabelerMap(modservices) return getLabelGroupToLabelerMap(labelers)
}, [modservices]) }, [labelers])
React.useEffect(() => { React.useEffect(() => {
// on initial load, show a loading spinner for a hot sec to prevent flash // 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 */} {/* Here to capture focus for a hot sec to prevent flash */}
<Pressable accessible={false} /> <Pressable accessible={false} />
</View> </View>
) : error || !(preferences && modservices) ? null : ( // TODO ) : error || !(preferences && labelers) ? null : ( // TODO
<ReportDialogLoaded <ReportDialogLoaded
{...props} {...props}
labelers={modservices} labelers={labelers}
labelGroupToLabelerMap={labelGroupToLabelerMap} 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 { import {
LabelsOnMeDialog, LabelsOnMeDialog,
useLabelsOnMeDialogControl, useLabelsOnMeDialogControl,
} from '#/components/LabelsOnMeDialog' } from '#/components/moderation/LabelsOnMeDialog'
export function LabelsOnMe({ export function LabelsOnMe({
details, details,
@@ -4,11 +4,15 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {ComAtprotoLabelDefs} from '@atproto/api' 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 {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import * as Dialog from '#/components/Dialog' import * as Dialog from '#/components/Dialog'
import {Button} from '#/components/Button' import {Button, ButtonText} from '#/components/Button'
import {capitalize} from '#/lib/strings/capitalize' import {InlineLink} from '#/components/Link'
export {useDialogControl as useLabelsOnMeDialogControl} from '#/components/Dialog' export {useDialogControl as useLabelsOnMeDialogControl} from '#/components/Dialog'
@@ -61,12 +65,13 @@ export function LabelsOnMeDialogInner(props: LabelsOnMeDialogProps) {
<Text <Text
nativeID="dialog-title" nativeID="dialog-title"
style={[a.text_2xl, a.font_bold, a.pb_md, a.leading_tight]}> style={[a.text_2xl, a.font_bold, a.pb_md, a.leading_tight]}>
<Trans> {isAccount ? (
The following labels were applied to your{' '} <Trans>Labels on your account</Trans>
{isAccount ? 'account' : 'content'} ) : (
</Trans> <Trans>Labels on your content</Trans>
)}
</Text> </Text>
<Text nativeID="dialog-description" style={[a.text_md, a.leading_snug]}> <Text nativeID="dialog-description" style={[a.text_sm, a.leading_snug]}>
<Trans> <Trans>
You may appeal these labels if you feel they were placed in error. You may appeal these labels if you feel they were placed in error.
</Trans> </Trans>
@@ -74,11 +79,11 @@ export function LabelsOnMeDialogInner(props: LabelsOnMeDialogProps) {
<View style={[a.py_lg, a.gap_md]}> <View style={[a.py_lg, a.gap_md]}>
{labels.map(label => ( {labels.map(label => (
<View <Label
key={`${label.src}-${label.val}`} key={`${label.val}-${label.src}`}
style={[a.p_md, a.rounded_sm, t.atoms.bg_contrast_25]}> label={label}
<Text>{capitalize(label.val)}</Text> control={props.control}
</View> />
))} ))}
</View> </View>
@@ -106,3 +111,49 @@ export function LabelsOnMeDialog(props: LabelsOnMeDialogProps) {
</Dialog.Outer> </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 {useLingui} from '@lingui/react'
import {ModerationCause} from '@atproto/api' 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 {Text} from '#/components/Typography'
import * as Dialog from '#/components/Dialog' import * as Dialog from '#/components/Dialog'
import {Button} from '#/components/Button' import {Button} from '#/components/Button'
import {InlineLink} from '#/components/Link' import {InlineLink} from '#/components/Link'
import {useLabelStrings} from '#/lib/moderation/useLabelStrings' import {makeProfileLink} from '#/lib/routes/links'
import {listUriToHref} from '#/lib/strings/url-helpers'
export {useDialogControl as useModerationDetailsDialogControl} from '#/components/Dialog' export {useDialogControl as useModerationDetailsDialogControl} from '#/components/Dialog'
export interface ModerationDetailsDialogProps { export interface ModerationDetailsDialogProps {
control: Dialog.DialogOuterProps['control'] control: Dialog.DialogOuterProps['control']
context: 'account' | 'content'
modcause: ModerationCause modcause: ModerationCause
} }
@@ -24,21 +25,20 @@ export function ModerationDetailsDialog(props: ModerationDetailsDialogProps) {
return ( return (
<Dialog.Outer control={props.control}> <Dialog.Outer control={props.control}>
<Dialog.Handle /> <Dialog.Handle />
<ModerationDetailsDialogInner {...props} /> <ModerationDetailsDialogInner {...props} />
</Dialog.Outer> </Dialog.Outer>
) )
} }
function ModerationDetailsDialogInner({ function ModerationDetailsDialogInner({
context,
modcause, modcause,
control,
}: ModerationDetailsDialogProps & { }: ModerationDetailsDialogProps & {
control: Dialog.DialogOuterProps['control'] control: Dialog.DialogOuterProps['control']
}) { }) {
const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
const labelStrings = useLabelStrings() const desc = useModerationCauseDescription(modcause)
const control = Dialog.useDialogControl()
const {gtMobile} = useBreakpoints() const {gtMobile} = useBreakpoints()
let name let name
@@ -95,13 +95,8 @@ function ModerationDetailsDialogInner({
description = _(msg`You have muted this user.`) description = _(msg`You have muted this user.`)
} }
} else if (modcause.type === 'label') { } else if (modcause.type === 'label') {
if (modcause.labelDef.id in labelStrings) { name = desc.name
name = labelStrings[modcause.labelDef.id][context].name description = desc.description
description = labelStrings[modcause.labelDef.id][context].description
} else {
name = modcause.labelDef.id
description = _(msg`Labeled ${modcause.labelDef.id}`)
}
} else { } else {
// should never happen // should never happen
name = '' name = ''
@@ -112,12 +107,38 @@ function ModerationDetailsDialogInner({
<Dialog.ScrollableInner <Dialog.ScrollableInner
accessibilityDescribedBy="dialog-description" accessibilityDescribedBy="dialog-description"
accessibilityLabelledBy="dialog-title"> 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} {name}
</Text> </Text>
<Text nativeID="dialog-description" style={[a.text_sm]}> <Text
nativeID="dialog-description"
style={[t.atoms.text, a.text_md, a.mb_md]}>
{description} {description}
</Text> </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]}> <View style={gtMobile && [a.flex_row, a.justify_end]}>
<Button <Button
testID="doneBtn" testID="doneBtn"
@@ -1,20 +1,18 @@
import React from 'react' import React from 'react'
import {StyleProp, View, ViewStyle} from 'react-native' import {StyleProp, View, ViewStyle} from 'react-native'
import {ModerationUI, ModerationCause} from '@atproto/api' import {ModerationUI, ModerationCause} from '@atproto/api'
import {Text} from '../text/Text'
import {Trans} from '@lingui/macro'
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription' import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
import {getModerationCauseKey} from '#/lib/moderation' import {getModerationCauseKey} from '#/lib/moderation'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonText, ButtonIcon} from '#/components/Button' import {Button, ButtonText, ButtonIcon} from '#/components/Button'
import {Shield_Stroke2_Corner0_Rounded as Shield} from '#/components/icons/Shield' import {Text} from '#/components/Typography'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash'
import { import {
ModerationDetailsDialog, ModerationDetailsDialog,
useModerationDetailsDialogControl, useModerationDetailsDialogControl,
} from '#/components/ModerationDetailsDialog' } from '#/components/moderation/ModerationDetailsDialog'
export function PostAlerts({ export function PostAlerts({
modui, modui,
@@ -46,7 +44,7 @@ export function PostAlerts({
function PostInform({cause}: {cause: ModerationCause}) { function PostInform({cause}: {cause: ModerationCause}) {
const control = useModerationDetailsDialogControl() const control = useModerationDetailsDialogControl()
const desc = useModerationCauseDescription(cause, 'content') const desc = useModerationCauseDescription(cause)
return ( return (
<> <>
@@ -59,18 +57,11 @@ function PostInform({cause}: {cause: ModerationCause}) {
onPress={() => { onPress={() => {
control.open() control.open()
}}> }}>
<ButtonIcon <ButtonIcon icon={desc.icon} position="left" />{' '}
icon={cause.type === 'muted' ? EyeSlash : CircleInfo}
position="left"
/>{' '}
<ButtonText>{desc.name}</ButtonText> <ButtonText>{desc.name}</ButtonText>
</Button> </Button>
<ModerationDetailsDialog <ModerationDetailsDialog control={control} modcause={cause} />
control={control}
context="content"
modcause={cause}
/>
</> </>
) )
} }
@@ -78,7 +69,7 @@ function PostInform({cause}: {cause: ModerationCause}) {
function PostAlert({cause}: {cause: ModerationCause}) { function PostAlert({cause}: {cause: ModerationCause}) {
const t = useTheme() const t = useTheme()
const control = useModerationDetailsDialogControl() const control = useModerationDetailsDialogControl()
const desc = useModerationCauseDescription(cause, 'content') const desc = useModerationCauseDescription(cause)
return ( return (
<> <>
@@ -91,21 +82,19 @@ function PostAlert({cause}: {cause: ModerationCause}) {
onPress={() => { onPress={() => {
control.open() control.open()
}}> }}>
<ButtonIcon icon={Shield} position="left" /> <ButtonIcon icon={desc.icon} position="left" />
<ButtonText style={[a.flex_1, a.text_left]}> <ButtonText style={[a.flex_1, a.text_left]}>
{desc.name} {desc.name}
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}> {desc.source && (
{' — ' /* TODO get actual labeler */} <Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
<Trans>Bluesky Safety</Trans> {' — '}
</Text> {sanitizeDisplayName(desc.source)}
</Text>
)}
</ButtonText> </ButtonText>
</Button> </Button>
<ModerationDetailsDialog <ModerationDetailsDialog control={control} modcause={cause} />
control={control}
context="content"
modcause={cause}
/>
</> </>
) )
} }
@@ -1,20 +1,20 @@
import React, {ComponentProps} from 'react' import React, {ComponentProps} from 'react'
import {StyleSheet, Pressable, View, ViewStyle, StyleProp} from 'react-native' import {StyleSheet, Pressable, View, ViewStyle, StyleProp} from 'react-native'
import {ModerationUI} from '@atproto/api' 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 {useLingui} from '@lingui/react'
import {Trans, msg} from '@lingui/macro' 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 { import {
ModerationDetailsDialog, ModerationDetailsDialog,
useModerationDetailsDialogControl, 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> { interface Props extends ComponentProps<typeof Link> {
iconSize: number iconSize: number
@@ -32,12 +32,12 @@ export function PostHider({
iconStyles, iconStyles,
...props ...props
}: Props) { }: Props) {
const pal = usePalette('default') const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
const [override, setOverride] = React.useState(false) const [override, setOverride] = React.useState(false)
const control = useModerationDetailsDialogControl() const control = useModerationDetailsDialogControl()
const blur = modui.blurs[0] const blur = modui.blurs[0]
const desc = useModerationCauseDescription(blur, 'content') const desc = useModerationCauseDescription(blur)
if (!blur) { if (!blur) {
return ( return (
@@ -45,7 +45,6 @@ export function PostHider({
testID={testID} testID={testID}
style={style} style={style}
href={href} href={href}
noFeedback
accessible={false} accessible={false}
{...props}> {...props}>
{children} {children}
@@ -53,7 +52,6 @@ export function PostHider({
) )
} }
const isMute = blur.type === 'muted'
return !override ? ( return !override ? (
<Pressable <Pressable
onPress={() => { onPress={() => {
@@ -67,15 +65,18 @@ export function PostHider({
} }
accessibilityLabel="" accessibilityLabel=""
style={[ style={[
styles.description, a.flex_row,
a.align_center,
a.gap_sm,
a.py_md,
{
paddingLeft: 6,
paddingRight: 18,
},
override ? {paddingBottom: 0} : undefined, override ? {paddingBottom: 0} : undefined,
pal.view, t.atoms.bg,
]}> ]}>
<ModerationDetailsDialog <ModerationDetailsDialog control={control} modcause={blur} />
control={control}
context="content"
modcause={blur}
/>
<Pressable <Pressable
onPress={() => { onPress={() => {
control.open() control.open()
@@ -85,32 +86,24 @@ export function PostHider({
accessibilityHint=""> accessibilityHint="">
<View <View
style={[ style={[
pal.viewLight, t.atoms.bg_contrast_25,
a.align_center,
a.justify_center,
{ {
width: iconSize, width: iconSize,
height: iconSize, height: iconSize,
borderRadius: iconSize, borderRadius: iconSize,
alignItems: 'center',
justifyContent: 'center',
}, },
iconStyles, iconStyles,
]}> ]}>
{isMute ? ( <desc.icon size="sm" fill={t.atoms.text_contrast_medium.color} />
<FontAwesomeIcon
icon={['far', 'eye-slash']}
size={14}
color={pal.colors.textLight}
/>
) : (
<ShieldExclamation size={14} style={pal.textLight} />
)}
</View> </View>
</Pressable> </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} {desc.name}
</Text> </Text>
{!modui.noOverride && ( {!modui.noOverride && (
<Text type="sm" style={[styles.showBtn, pal.link]}> <Text style={[{color: t.palette.primary_500}]}>
{override ? <Trans>Hide</Trans> : <Trans>Show</Trans>} {override ? <Trans>Hide</Trans> : <Trans>Show</Trans>}
</Text> </Text>
)} )}
@@ -120,26 +113,14 @@ export function PostHider({
testID={testID} testID={testID}
style={addStyle(style, styles.child)} style={addStyle(style, styles.child)}
href={href} href={href}
noFeedback> accessible={false}
{...props}>
{children} {children}
</Link> </Link>
) )
} }
const styles = StyleSheet.create({ 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: { child: {
borderWidth: 0, borderWidth: 0,
borderTopWidth: 0, borderTopWidth: 0,
@@ -1,20 +1,18 @@
import React from 'react' import React from 'react'
import {StyleProp, View, ViewStyle} from 'react-native' import {StyleProp, View, ViewStyle} from 'react-native'
import {ModerationCause, ModerationDecision} from '@atproto/api' import {ModerationCause, ModerationDecision} from '@atproto/api'
import {Text} from '../text/Text'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {getModerationCauseKey} from 'lib/moderation' import {getModerationCauseKey} from 'lib/moderation'
import {Trans} from '@lingui/macro'
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription' import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonText, ButtonIcon} from '#/components/Button' import {Button, ButtonText, ButtonIcon} from '#/components/Button'
import {Shield_Stroke2_Corner0_Rounded as Shield} from '#/components/icons/Shield' import {Text} from '#/components/Typography'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash'
import { import {
ModerationDetailsDialog, ModerationDetailsDialog,
useModerationDetailsDialogControl, useModerationDetailsDialogControl,
} from '#/components/ModerationDetailsDialog' } from '#/components/moderation/ModerationDetailsDialog'
export function ProfileHeaderAlerts({ export function ProfileHeaderAlerts({
moderation, moderation,
@@ -46,7 +44,7 @@ export function ProfileHeaderAlerts({
function ProfileInform({cause}: {cause: ModerationCause}) { function ProfileInform({cause}: {cause: ModerationCause}) {
const control = useModerationDetailsDialogControl() const control = useModerationDetailsDialogControl()
const desc = useModerationCauseDescription(cause, 'account') const desc = useModerationCauseDescription(cause)
return ( return (
<> <>
@@ -59,18 +57,11 @@ function ProfileInform({cause}: {cause: ModerationCause}) {
onPress={() => { onPress={() => {
control.open() control.open()
}}> }}>
<ButtonIcon <ButtonIcon icon={desc.icon} position="left" />{' '}
icon={cause.type === 'muted' ? EyeSlash : CircleInfo}
position="left"
/>{' '}
<ButtonText>{desc.name}</ButtonText> <ButtonText>{desc.name}</ButtonText>
</Button> </Button>
<ModerationDetailsDialog <ModerationDetailsDialog control={control} modcause={cause} />
control={control}
context="account"
modcause={cause}
/>
</> </>
) )
} }
@@ -78,7 +69,7 @@ function ProfileInform({cause}: {cause: ModerationCause}) {
function ProfileAlert({cause}: {cause: ModerationCause}) { function ProfileAlert({cause}: {cause: ModerationCause}) {
const t = useTheme() const t = useTheme()
const control = useModerationDetailsDialogControl() const control = useModerationDetailsDialogControl()
const desc = useModerationCauseDescription(cause, 'account') const desc = useModerationCauseDescription(cause)
return ( return (
<> <>
@@ -91,20 +82,18 @@ function ProfileAlert({cause}: {cause: ModerationCause}) {
onPress={() => { onPress={() => {
control.open() control.open()
}}> }}>
<ButtonIcon icon={Shield} position="left" /> <ButtonIcon icon={desc.icon} position="left" />
<ButtonText style={[a.flex_1, a.text_left]}> <ButtonText style={[a.flex_1, a.text_left]}>
{desc.name} {desc.name}
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}> {desc.source && (
{' — ' /* TODO get actual labeler */} <Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
<Trans>Bluesky Safety</Trans> {' — '}
</Text> {sanitizeDisplayName(desc.source)}
</Text>
)}
</ButtonText> </ButtonText>
</Button> </Button>
<ModerationDetailsDialog <ModerationDetailsDialog control={control} modcause={cause} />
control={control}
context="account"
modcause={cause}
/>
</> </>
) )
} }
@@ -2,31 +2,26 @@ import React from 'react'
import { import {
TouchableWithoutFeedback, TouchableWithoutFeedback,
StyleProp, StyleProp,
StyleSheet,
View, View,
ViewStyle, ViewStyle,
} from 'react-native' } from 'react-native'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import {useNavigation} from '@react-navigation/native' import {useNavigation} from '@react-navigation/native'
import {ModerationUI} from '@atproto/api' 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 {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' 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 { import {
ModerationDetailsDialog, ModerationDetailsDialog,
useModerationDetailsDialogControl, useModerationDetailsDialogControl,
} from '#/components/ModerationDetailsDialog' } from '#/components/moderation/ModerationDetailsDialog'
export function ScreenHider({ export function ScreenHider({
testID, testID,
@@ -42,15 +37,14 @@ export function ScreenHider({
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
containerStyle?: StyleProp<ViewStyle> containerStyle?: StyleProp<ViewStyle>
}>) { }>) {
const pal = usePalette('default') const t = useTheme()
const palInverted = usePalette('inverted')
const {_} = useLingui() const {_} = useLingui()
const [override, setOverride] = React.useState(false) const [override, setOverride] = React.useState(false)
const navigation = useNavigation<NavigationProp>() const navigation = useNavigation<NavigationProp>()
const {isMobile} = useWebMediaQueries() const {isMobile} = useWebMediaQueries()
const control = useModerationDetailsDialogControl() const control = useModerationDetailsDialogControl()
const blur = modui.blurs[0] const blur = modui.blurs[0]
const desc = useModerationCauseDescription(blur, 'content') const desc = useModerationCauseDescription(blur)
if (!blur || override) { if (!blur || override) {
return ( return (
@@ -66,25 +60,53 @@ export function ScreenHider({
) )
return ( return (
<CenteredView <CenteredView
style={[styles.container, pal.view, containerStyle]} style={[
a.flex_1,
{
paddingTop: 100,
paddingBottom: 150,
},
t.atoms.bg,
containerStyle,
]}
sideBorders> sideBorders>
<View style={styles.iconContainer}> <View style={[a.align_center, a.mb_md]}>
<View style={[styles.icon, palInverted.view]}> <View
<FontAwesomeIcon style={[
icon={isNoPwi ? ['far', 'eye-slash'] : 'exclamation'} t.atoms.bg_contrast_975,
style={pal.textInverted as FontAwesomeIconStyle} a.align_center,
size={24} a.justify_center,
/> {
borderRadius: 25,
width: 50,
height: 50,
},
]}>
<desc.icon width={24} fill={t.atoms.bg.backgroundColor} />
</View> </View>
</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 ? ( {isNoPwi ? (
<Trans>Sign-in Required</Trans> <Trans>Sign-in Required</Trans>
) : ( ) : (
<Trans>Content Warning</Trans> <Trans>Content Warning</Trans>
)} )}
</Text> </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 ? ( {isNoPwi ? (
<Trans> <Trans>
This account has requested that users sign in to view their profile. 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> <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}.{' '} {desc.name}.{' '}
</Text> </Text>
<TouchableWithoutFeedback <TouchableWithoutFeedback
@@ -102,87 +124,48 @@ export function ScreenHider({
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={_(msg`Learn more about this warning`)} accessibilityLabel={_(msg`Learn more about this warning`)}
accessibilityHint=""> accessibilityHint="">
<Text type="2xl" style={pal.link}> <Text style={[a.text_lg, {color: t.palette.primary_500}]}>
<Trans>Learn More</Trans> <Trans>Learn More</Trans>
</Text> </Text>
</TouchableWithoutFeedback> </TouchableWithoutFeedback>
<ModerationDetailsDialog <ModerationDetailsDialog control={control} modcause={blur} />
control={control}
context="account"
modcause={blur}
/>
</> </>
)}{' '} )}{' '}
</Text> </Text>
{isMobile && <View style={styles.spacer} />} {isMobile && <View style={a.flex_1} />}
<View style={styles.btnContainer}> <View style={[a.flex_row, a.justify_center, a.my_md, a.gap_md]}>
<Button <Button
type="inverted" variant="solid"
color="primary"
size="large"
style={[a.rounded_full]}
label={_(msg`Go back`)}
onPress={() => { onPress={() => {
if (navigation.canGoBack()) { if (navigation.canGoBack()) {
navigation.goBack() navigation.goBack()
} else { } else {
navigation.navigate('Home') navigation.navigate('Home')
} }
}} }}>
style={styles.btn}> <ButtonText>
<Text type="button-lg" style={pal.textInverted}>
<Trans>Go back</Trans> <Trans>Go back</Trans>
</Text> </ButtonText>
</Button> </Button>
{!modui.noOverride && ( {!modui.noOverride && (
<Button <Button
type="default" variant="solid"
onPress={() => setOverride(v => !v)} color="secondary"
style={styles.btn}> size="large"
<Text type="button-lg" style={pal.text}> style={[a.rounded_full]}
label={_(msg`Show anyway`)}
onPress={() => setOverride(v => !v)}>
<ButtonText>
<Trans>Show anyway</Trans> <Trans>Show anyway</Trans>
</Text> </ButtonText>
</Button> </Button>
)} )}
</View> </View>
</CenteredView> </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 {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' 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 { export interface ModerationCauseDescription {
icon: any
name: string name: string
description: string description: string
source?: string
} }
export function useModerationCauseDescription( export function useModerationCauseDescription(
cause: ModerationCause | undefined, cause: ModerationCause | undefined,
context: 'account' | 'content',
): ModerationCauseDescription { ): ModerationCauseDescription {
const {_} = useLingui() const {_, i18n} = useLingui()
const labelStrings = useLabelStrings() const globalLabelStrings = useGlobalLabelStrings()
const {labelDefs, labelers} = useLabelDefinitions()
if (!cause) { if (!cause) {
return { return {
icon: TriangleExclamation,
name: _(msg`Content Warning`), name: _(msg`Content Warning`),
description: _( description: _(
msg`Moderator has chosen to set a general warning on the content.`, 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.type === 'blocking') {
if (cause.source.type === 'list') { if (cause.source.type === 'list') {
return { return {
icon: CircleBanSign,
name: _(msg`User Blocked by "${cause.source.list.name}"`), name: _(msg`User Blocked by "${cause.source.list.name}"`),
description: _( description: _(
msg`You have blocked this user. You cannot view their content.`, msg`You have blocked this user. You cannot view their content.`,
@@ -32,6 +43,7 @@ export function useModerationCauseDescription(
} }
} else { } else {
return { return {
icon: CircleBanSign,
name: _(msg`User Blocked`), name: _(msg`User Blocked`),
description: _( description: _(
msg`You have blocked this user. You cannot view their content.`, msg`You have blocked this user. You cannot view their content.`,
@@ -41,6 +53,7 @@ export function useModerationCauseDescription(
} }
if (cause.type === 'blocked-by') { if (cause.type === 'blocked-by') {
return { return {
icon: CircleBanSign,
name: _(msg`User Blocking You`), name: _(msg`User Blocking You`),
description: _( description: _(
msg`This user has blocked you. You cannot view their content.`, msg`This user has blocked you. You cannot view their content.`,
@@ -49,6 +62,7 @@ export function useModerationCauseDescription(
} }
if (cause.type === 'block-other') { if (cause.type === 'block-other') {
return { return {
icon: CircleBanSign,
name: _(msg`Content Not Available`), name: _(msg`Content Not Available`),
description: _( description: _(
msg`This content is not available because one of the users involved has blocked the other.`, 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.type === 'muted') {
if (cause.source.type === 'list') { if (cause.source.type === 'list') {
return { return {
icon: EyeSlash,
name: _(msg`Muted by "${cause.source.list.name}"`), name: _(msg`Muted by "${cause.source.list.name}"`),
description: _(msg`You have muted this user`), description: _(msg`You have muted this user`),
} }
} else { } else {
return { return {
icon: EyeSlash,
name: _(msg`Muted User`), name: _(msg`Muted User`),
description: _(msg`You have muted this user`), description: _(msg`You have muted this user`),
} }
@@ -71,30 +87,30 @@ export function useModerationCauseDescription(
// @ts-ignore Temporary extension to the moderation system -prf // @ts-ignore Temporary extension to the moderation system -prf
if (cause.type === 'hidden') { if (cause.type === 'hidden') {
return { return {
icon: EyeSlash,
name: _(msg`Post Hidden by You`), name: _(msg`Post Hidden by You`),
description: _(msg`You have hidden this post`), description: _(msg`You have hidden this post`),
} }
} }
if (cause.type === 'label') { if (cause.type === 'label') {
if (cause.labelDef.identifier in labelStrings) { const def = getDefinition(labelDefs, cause.label)
const strings = const strings = getLabelStrings(i18n.locale, globalLabelStrings, def)
labelStrings[cause.labelDef.identifier as keyof typeof LABELS] const labeler = labelers.find(l => l.creator.did === cause.label.src)
return {
name:
context === 'account' ? strings.account.name : strings.content.name,
description:
context === 'account'
? strings.account.description
: strings.content.description,
}
}
return { return {
name: cause.labelDef.identifier, icon:
description: _(msg`Labeled ${cause.labelDef.identifier}`), 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 // should never happen
return { return {
icon: CircleInfo,
name: '', name: '',
description: ``, description: ``,
} }
+18 -35
View File
@@ -82,17 +82,17 @@ export function ModerationScreen(
data: preferences, data: preferences,
} = usePreferencesQuery() } = usePreferencesQuery()
const { const {
isLoading: isModServicesLoading, isLoading: isLabelersLoading,
data: modservices, data: labelers,
error: modservicesError, error: labelersError,
} = useLabelersDetailedInfoQuery({ } = useLabelersDetailedInfoQuery({
dids: preferences ? preferences.moderationPrefs.mods.map(m => m.did) : [], dids: preferences ? preferences.moderationPrefs.mods.map(m => m.did) : [],
}) })
const {gtMobile} = useBreakpoints() const {gtMobile} = useBreakpoints()
const {height} = useSafeAreaFrame() const {height} = useSafeAreaFrame()
const isLoading = isPreferencesLoading || isModServicesLoading const isLoading = isPreferencesLoading || isLabelersLoading
const error = preferencesError || modservicesError const error = preferencesError || labelersError
return ( return (
<CenteredView <CenteredView
@@ -109,7 +109,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 && modservices) ? ( ) : error || !(preferences && labelers) ? (
<ErrorState <ErrorState
error={ error={
preferencesError?.toString() || preferencesError?.toString() ||
@@ -117,10 +117,7 @@ export function ModerationScreen(
} }
/> />
) : ( ) : (
<ModerationScreenInner <ModerationScreenInner preferences={preferences} labelers={labelers} />
preferences={preferences}
modservices={modservices}
/>
)} )}
</CenteredView> </CenteredView>
) )
@@ -128,10 +125,10 @@ export function ModerationScreen(
export function ModerationScreenInner({ export function ModerationScreenInner({
preferences, preferences,
modservices, labelers,
}: { }: {
preferences: UsePreferencesQueryResponse preferences: UsePreferencesQueryResponse
modservices: AppBskyLabelerDefs.LabelerViewDetailed[] labelers: AppBskyLabelerDefs.LabelerViewDetailed[]
}) { }) {
const t = useTheme() const t = useTheme()
const setMinimalShellMode = useSetMinimalShellMode() const setMinimalShellMode = useSetMinimalShellMode()
@@ -297,25 +294,13 @@ export function ModerationScreenInner({
{adultContentEnabled && ( {adultContentEnabled && (
<> <>
<Divider /> <Divider />
<SimpleModerationLabelPref <SimpleModerationLabelPref labelValueDefinition={LABELS.porn} />
labelValueDefinition={LABELS.porn}
labelerDid={undefined}
/>
<Divider /> <Divider />
<SimpleModerationLabelPref <SimpleModerationLabelPref labelValueDefinition={LABELS.sexual} />
labelValueDefinition={LABELS.sexual}
labelerDid={undefined}
/>
<Divider /> <Divider />
<SimpleModerationLabelPref <SimpleModerationLabelPref labelValueDefinition={LABELS.nudity} />
labelValueDefinition={LABELS.nudity}
labelerDid={undefined}
/>
<Divider /> <Divider />
<SimpleModerationLabelPref <SimpleModerationLabelPref labelValueDefinition={LABELS.gore} />
labelValueDefinition={LABELS.gore}
labelerDid={undefined}
/>
</> </>
)} )}
</View> </View>
@@ -332,7 +317,7 @@ export function ModerationScreenInner({
</Text> </Text>
<View style={[a.rounded_sm, t.atoms.bg_contrast_25]}> <View style={[a.rounded_sm, t.atoms.bg_contrast_25]}>
<ModerationServiceCard.Link <ModerationServiceCard.Link
modservice={{ labeler={{
uri: '', uri: '',
cid: '', cid: '',
policies: { policies: {
@@ -357,13 +342,11 @@ export function ModerationScreenInner({
/> />
</ModerationServiceCard.Card.Outer> </ModerationServiceCard.Card.Outer>
</ModerationServiceCard.Link> </ModerationServiceCard.Link>
{modservices.map(mod => { {labelers.map(mod => {
return ( return (
<> <React.Fragment key={mod.creator.did}>
<Divider /> <Divider />
<ModerationServiceCard.Link <ModerationServiceCard.Link labeler={mod}>
modservice={mod}
key={mod.creator.did}>
<ModerationServiceCard.Card.Outer> <ModerationServiceCard.Card.Outer>
<ModerationServiceCard.Card.Avatar <ModerationServiceCard.Card.Avatar
avatar={mod.creator.avatar} avatar={mod.creator.avatar}
@@ -378,7 +361,7 @@ export function ModerationScreenInner({
/> />
</ModerationServiceCard.Card.Outer> </ModerationServiceCard.Card.Outer>
</ModerationServiceCard.Link> </ModerationServiceCard.Link>
</> </React.Fragment>
) )
})} })}
</View> </View>
+2 -2
View File
@@ -13,12 +13,12 @@ import {Shadow} from '#/state/cache/types'
import {useLightboxControls, ProfileImageLightbox} from '#/state/lightbox' import {useLightboxControls, ProfileImageLightbox} from '#/state/lightbox'
import {atoms as a, useTheme} from '#/alf' 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 {BlurView} from 'view/com/util/BlurView'
import {LoadingPlaceholder} from 'view/com/util/LoadingPlaceholder' import {LoadingPlaceholder} from 'view/com/util/LoadingPlaceholder'
import {UserAvatar} from 'view/com/util/UserAvatar' import {UserAvatar} from 'view/com/util/UserAvatar'
import {UserBanner} from 'view/com/util/UserBanner' import {UserBanner} from 'view/com/util/UserBanner'
import {ProfileHeaderAlerts} from 'view/com/util/moderation/ProfileHeaderAlerts' import {ProfileHeaderAlerts} from '#/components/moderation/ProfileHeaderAlerts'
interface Props { interface Props {
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed> profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>
+7 -3
View File
@@ -8,7 +8,7 @@ import {
import {track} from '#/lib/analytics/analytics' import {track} from '#/lib/analytics/analytics'
import {getAge} from '#/lib/strings/time' import {getAge} from '#/lib/strings/time'
import {getAgent} from '#/state/session' import {getAgent, useSession} from '#/state/session'
import { import {
ConfigurableLabelGroup, ConfigurableLabelGroup,
UsePreferencesQueryResponse, UsePreferencesQueryResponse,
@@ -21,6 +21,7 @@ import {
} from '#/state/queries/preferences/const' } from '#/state/queries/preferences/const'
import {STALE} from '#/state/queries' import {STALE} from '#/state/queries'
import {useHiddenPosts} from '#/state/preferences/hidden-posts' 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/types'
export * from '#/state/queries/preferences/moderation' export * from '#/state/queries/preferences/moderation'
@@ -74,7 +75,9 @@ export const moderationOptsOverrideContext = createContext<
export function useModerationOpts() { export function useModerationOpts() {
const override = useContext(moderationOptsOverrideContext) const override = useContext(moderationOptsOverrideContext)
const {currentAccount} = useSession()
const prefs = usePreferencesQuery() const prefs = usePreferencesQuery()
const {labelDefs} = useLabelDefinitions()
const hiddenPosts = useHiddenPosts() const hiddenPosts = useHiddenPosts()
const opts = useMemo<ModerationOpts | undefined>(() => { const opts = useMemo<ModerationOpts | undefined>(() => {
if (override) { if (override) {
@@ -85,13 +88,14 @@ export function useModerationOpts() {
} }
const moderationPrefs = prefs.data.moderationPrefs const moderationPrefs = prefs.data.moderationPrefs
return { return {
userDid: '', // TODO userDid: currentAccount?.did,
prefs: { prefs: {
...moderationPrefs, ...moderationPrefs,
hiddenPosts, hiddenPosts,
}, },
labelDefs,
} }
}, [override, prefs.data, hiddenPosts]) }, [override, currentAccount, labelDefs, prefs.data, hiddenPosts])
return opts 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 { import {
LabelGroup, LabelGroup,
ConfigurableLabelGroup, ConfigurableLabelGroup,
} from '#/state/queries/preferences/types' } from '#/state/queries/preferences/types'
import {usePreferencesQuery} from './index'
export type Label = ComAtprotoLabelDefs.Label import {useLabelersDetailedInfoQuery} from '../labeler'
import {
useGlobalLabelStrings,
GlobalLabelStrings,
} from '#/lib/moderation/useGlobalLabelStrings'
export type LabelGroupConfig = { export type LabelGroupConfig = {
id: LabelGroup id: LabelGroup
@@ -18,8 +33,6 @@ export type LabelGroupConfig = {
/** /**
* More strict than our default settings for logged in users. * More strict than our default settings for logged in users.
*
* TODO(pwi)
*/ */
export const DEFAULT_LOGGED_OUT_LABEL_PREFERENCES: typeof DEFAULT_LABEL_SETTINGS = export const DEFAULT_LOGGED_OUT_LABEL_PREFERENCES: typeof DEFAULT_LABEL_SETTINGS =
Object.fromEntries( Object.fromEntries(
@@ -84,3 +97,26 @@ export const CONFIGURABLE_LABEL_GROUPS: Record<
values: ['impersonation'], 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 {PostMeta} from '../util/PostMeta'
import {PostEmbeds} from '../util/post-embeds' import {PostEmbeds} from '../util/post-embeds'
import {PostCtrls} from '../util/post-ctrls/PostCtrls' import {PostCtrls} from '../util/post-ctrls/PostCtrls'
import {PostHider} from '../util/moderation/PostHider' import {PostHider} from '../../../components/moderation/PostHider'
import {ContentHider} from '../util/moderation/ContentHider' import {ContentHider} from '../../../components/moderation/ContentHider'
import {PostAlerts} from '../util/moderation/PostAlerts' import {PostAlerts} from '../../../components/moderation/PostAlerts'
import {LabelsOnMyPost} from '../util/moderation/LabelsOnMe' import {LabelsOnMyPost} from '../../../components/moderation/LabelsOnMe'
import {ErrorMessage} from '../util/error/ErrorMessage' import {ErrorMessage} from '../util/error/ErrorMessage'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {formatCount} from '../util/numeric/format' 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 {PostMeta} from '../util/PostMeta'
import {PostEmbeds} from '../util/post-embeds' import {PostEmbeds} from '../util/post-embeds'
import {PostCtrls} from '../util/post-ctrls/PostCtrls' import {PostCtrls} from '../util/post-ctrls/PostCtrls'
import {ContentHider} from '../util/moderation/ContentHider' import {ContentHider} from '../../../components/moderation/ContentHider'
import {PostAlerts} from '../util/moderation/PostAlerts' import {PostAlerts} from '../../../components/moderation/PostAlerts'
import {LabelsOnMyPost} from '../util/moderation/LabelsOnMe' import {LabelsOnMyPost} from '../../../components/moderation/LabelsOnMe'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import {RichText} from '#/components/RichText' import {RichText} from '#/components/RichText'
import {PreviewableUserAvatar} from '../util/UserAvatar' import {PreviewableUserAvatar} from '../util/UserAvatar'
+3 -3
View File
@@ -18,9 +18,9 @@ import {UserInfoText} from '../util/UserInfoText'
import {PostMeta} from '../util/PostMeta' import {PostMeta} from '../util/PostMeta'
import {PostCtrls} from '../util/post-ctrls/PostCtrls' import {PostCtrls} from '../util/post-ctrls/PostCtrls'
import {PostEmbeds} from '../util/post-embeds' import {PostEmbeds} from '../util/post-embeds'
import {ContentHider} from '../util/moderation/ContentHider' import {ContentHider} from '#/components/moderation/ContentHider'
import {PostAlerts} from '../util/moderation/PostAlerts' import {PostAlerts} from '../../../components/moderation/PostAlerts'
import {LabelsOnMyPost} from '../util/moderation/LabelsOnMe' import {LabelsOnMyPost} from '../../../components/moderation/LabelsOnMe'
import {RichText} from '#/components/RichText' import {RichText} from '#/components/RichText'
import {PreviewableUserAvatar} from '../util/UserAvatar' import {PreviewableUserAvatar} from '../util/UserAvatar'
import {s} from 'lib/styles' 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 {usePalette} from 'lib/hooks/usePalette'
import {ComposerOptsQuote} from 'state/shell/composer' import {ComposerOptsQuote} from 'state/shell/composer'
import {PostEmbeds} from '.' import {PostEmbeds} from '.'
import {PostAlerts} from '../moderation/PostAlerts' import {PostAlerts} from '../../../../components/moderation/PostAlerts'
import {makeProfileLink} from 'lib/routes/links' import {makeProfileLink} from 'lib/routes/links'
import {InfoCircleIcon} from 'lib/icons' import {InfoCircleIcon} from 'lib/icons'
import {Trans} from '@lingui/macro' import {Trans} from '@lingui/macro'
import {useModerationOpts} from '#/state/queries/preferences' import {useModerationOpts} from '#/state/queries/preferences'
import {ContentHider} from '../moderation/ContentHider' import {ContentHider} from '../../../../components/moderation/ContentHider'
import {RichText} from '#/components/RichText' import {RichText} from '#/components/RichText'
import {atoms as a} from '#/alf' import {atoms as a} from '#/alf'
+1 -1
View File
@@ -26,7 +26,7 @@ import {MaybeQuoteEmbed} from './QuoteEmbed'
import {AutoSizedImage} from '../images/AutoSizedImage' import {AutoSizedImage} from '../images/AutoSizedImage'
import {ListEmbed} from './ListEmbed' import {ListEmbed} from './ListEmbed'
import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard' import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard'
import {ContentHider} from '../moderation/ContentHider' import {ContentHider} from '../../../../components/moderation/ContentHider'
import {isNative} from '#/platform/detection' import {isNative} from '#/platform/detection'
import {shareUrl} from '#/lib/sharing' import {shareUrl} from '#/lib/sharing'
+4 -4
View File
@@ -28,7 +28,7 @@ import {
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {CenteredView, ScrollView} from '#/view/com/util/Views' import {CenteredView, ScrollView} from '#/view/com/util/Views'
import {H1, H3, P, Text} from '#/components/Typography' 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 Toggle from '#/components/forms/Toggle'
import * as ToggleButton from '#/components/forms/ToggleButton' import * as ToggleButton from '#/components/forms/ToggleButton'
import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {Button, ButtonIcon, ButtonText} from '#/components/Button'
@@ -37,7 +37,7 @@ import {
ChevronBottom_Stroke2_Corner0_Rounded as ChevronBottom, ChevronBottom_Stroke2_Corner0_Rounded as ChevronBottom,
ChevronTop_Stroke2_Corner0_Rounded as ChevronTop, ChevronTop_Stroke2_Corner0_Rounded as ChevronTop,
} from '#/components/icons/Chevron' } from '#/components/icons/Chevron'
import {ScreenHider} from '../com/util/moderation/ScreenHider' import {ScreenHider} from '../../components/moderation/ScreenHider'
import {ProfileHeader} from '#/screens/Profile/Header' import {ProfileHeader} from '#/screens/Profile/Header'
import {ProfileCard} from '../com/profile/ProfileCard' import {ProfileCard} from '../com/profile/ProfileCard'
import {FeedItem} from '../com/posts/FeedItem' import {FeedItem} from '../com/posts/FeedItem'
@@ -73,7 +73,7 @@ export const DebugModScreen = ({}: NativeStackScreenProps<
], ],
}) })
const [view, setView] = React.useState<string[]>(['post']) const [view, setView] = React.useState<string[]>(['post'])
const labelStrings = useLabelStrings() const labelStrings = useGlobalLabelStrings()
const {currentAccount} = useSession() const {currentAccount} = useSession()
const isTargetMe = const isTargetMe =
@@ -309,7 +309,7 @@ export const DebugModScreen = ({}: NativeStackScreenProps<
<Toggle.Item <Toggle.Item
key={labelValue} key={labelValue}
name={labelValue} name={labelValue}
label={labelStrings[labelValue].general.name} label={labelStrings[labelValue].name}
disabled={disabled} disabled={disabled}
style={disabled ? {opacity: 0.5} : undefined}> style={disabled ? {opacity: 0.5} : undefined}>
<Toggle.Radio /> <Toggle.Radio />
+1 -1
View File
@@ -12,7 +12,7 @@ import {useLingui} from '@lingui/react'
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
import {CenteredView} from '../com/util/Views' import {CenteredView} from '../com/util/Views'
import {ListRef} from '../com/util/List' 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 {ProfileLists} from '../com/lists/ProfileLists'
import {ProfileFeedgens} from '../com/feeds/ProfileFeedgens' import {ProfileFeedgens} from '../com/feeds/ProfileFeedgens'
import {PagerWithHeader} from 'view/com/pager/PagerWithHeader' import {PagerWithHeader} from 'view/com/pager/PagerWithHeader'