Implement label handling
This commit is contained in:
@@ -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}"`,
|
||||
|
||||
@@ -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} />
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
})
|
||||
+1
-1
@@ -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,
|
||||
+63
-12
@@ -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>
|
||||
)
|
||||
}
|
||||
+38
-17
@@ -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"
|
||||
+16
-27
@@ -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} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
+28
-47
@@ -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,
|
||||
+16
-27
@@ -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} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
+70
-87
@@ -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`),
|
||||
},
|
||||
}),
|
||||
[_],
|
||||
)
|
||||
}
|
||||
@@ -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}"`,
|
||||
}
|
||||
}
|
||||
@@ -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: ``,
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
})
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user