Implement label handling
This commit is contained in:
@@ -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}"`,
|
||||||
|
|||||||
@@ -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} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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 {
|
import {
|
||||||
LabelsOnMeDialog,
|
LabelsOnMeDialog,
|
||||||
useLabelsOnMeDialogControl,
|
useLabelsOnMeDialogControl,
|
||||||
} from '#/components/LabelsOnMeDialog'
|
} from '#/components/moderation/LabelsOnMeDialog'
|
||||||
|
|
||||||
export function LabelsOnMe({
|
export function LabelsOnMe({
|
||||||
details,
|
details,
|
||||||
+63
-12
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
+38
-17
@@ -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"
|
||||||
+16
-27
@@ -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}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+28
-47
@@ -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,
|
||||||
+16
-27
@@ -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}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+70
-87
@@ -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`),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[_],
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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 {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: ``,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -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'
|
||||||
|
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
Reference in New Issue
Block a user