Add graphic media self label (#5758)

Co-authored-by: Samuel Newman <mozzius@protonmail.com>
This commit is contained in:
Hailey
2024-10-16 19:17:22 -07:00
committed by GitHub
parent 3d9663db1e
commit 2e74f98392
4 changed files with 247 additions and 122 deletions
+65 -3
View File
@@ -4,9 +4,12 @@ import {ModerationUI} from '@atproto/api'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {isJustAMute} from '#/lib/moderation' import {ADULT_CONTENT_LABELS, isJustAMute} from '#/lib/moderation'
import {useGlobalLabelStrings} from '#/lib/moderation/useGlobalLabelStrings'
import {getDefinition, getLabelStrings} from '#/lib/moderation/useLabelInfo'
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription' import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {useLabelDefinitions} from '#/state/preferences'
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
import {Button} from '#/components/Button' import {Button} from '#/components/Button'
import { import {
@@ -34,10 +37,68 @@ export function ContentHider({
const {gtMobile} = useBreakpoints() const {gtMobile} = useBreakpoints()
const [override, setOverride] = React.useState(false) const [override, setOverride] = React.useState(false)
const control = useModerationDetailsDialogControl() const control = useModerationDetailsDialogControl()
const {labelDefs} = useLabelDefinitions()
const globalLabelStrings = useGlobalLabelStrings()
const {i18n} = useLingui()
const blur = modui?.blurs[0] const blur = modui?.blurs[0]
const desc = useModerationCauseDescription(blur) const desc = useModerationCauseDescription(blur)
const labelName = React.useMemo(() => {
if (!modui?.blurs || !blur) {
return undefined
}
if (
blur.type !== 'label' ||
(blur.type === 'label' && blur.source.type !== 'user')
) {
return desc.name
}
let hasAdultContentLabel = false
const selfBlurNames = modui.blurs
.filter(cause => {
if (cause.type !== 'label') {
return false
}
if (cause.source.type !== 'user') {
return false
}
if (ADULT_CONTENT_LABELS.includes(cause.label.val)) {
if (hasAdultContentLabel) {
return false
}
hasAdultContentLabel = true
}
return true
})
.slice(0, 2)
.map(cause => {
if (cause.type !== 'label') {
return
}
const def = cause.labelDef || getDefinition(labelDefs, cause.label)
if (def.identifier === 'porn' || def.identifier === 'sexual') {
return _(msg`Adult Content`)
}
return getLabelStrings(i18n.locale, globalLabelStrings, def).name
})
if (selfBlurNames.length === 0) {
return desc.name
}
return [...new Set(selfBlurNames)].join(', ')
}, [
_,
modui?.blurs,
blur,
desc.name,
labelDefs,
i18n.locale,
globalLabelStrings,
])
if (!blur || (ignoreMute && isJustAMute(modui))) { if (!blur || (ignoreMute && isJustAMute(modui))) {
return ( return (
<View testID={testID} style={style}> <View testID={testID} style={style}>
@@ -99,8 +160,9 @@ export function ContentHider({
web({ web({
marginBottom: 1, marginBottom: 1,
}), }),
]}> ]}
{desc.name} numberOfLines={2}>
{labelName}
</Text> </Text>
{!modui.noOverride && ( {!modui.noOverride && (
<Text <Text
+8
View File
@@ -14,6 +14,14 @@ import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles' import {sanitizeHandle} from '#/lib/strings/handles'
import {AppModerationCause} from '#/components/Pills' import {AppModerationCause} from '#/components/Pills'
export const ADULT_CONTENT_LABELS = ['sexual', 'nudity', 'porn']
export const OTHER_SELF_LABELS = ['graphic-media']
export const SELF_LABELS = [...ADULT_CONTENT_LABELS, ...OTHER_SELF_LABELS]
export type AdultSelfLabel = (typeof ADULT_CONTENT_LABELS)[number]
export type OtherSelfLabel = (typeof OTHER_SELF_LABELS)[number]
export type SelfLabel = (typeof SELF_LABELS)[number]
export function getModerationCauseKey( export function getModerationCauseKey(
cause: ModerationCause | AppModerationCause, cause: ModerationCause | AppModerationCause,
): string { ): string {
+171 -117
View File
@@ -1,44 +1,52 @@
import React from 'react' import React from 'react'
import {Keyboard, LayoutAnimation, View} from 'react-native' import {Keyboard, View} from 'react-native'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {ShieldExclamation} from '#/lib/icons' import {ShieldExclamation} from '#/lib/icons'
import {
ADULT_CONTENT_LABELS,
AdultSelfLabel,
OTHER_SELF_LABELS,
OtherSelfLabel,
SelfLabel,
} from '#/lib/moderation'
import {isWeb} from '#/platform/detection'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button' import {Button, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog' import * as Dialog from '#/components/Dialog'
import * as ToggleButton from '#/components/forms/ToggleButton' import * as Toggle from '#/components/forms/Toggle'
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
const ADULT_CONTENT_LABELS = ['sexual', 'nudity', 'porn']
export function LabelsBtn({ export function LabelsBtn({
labels, labels,
hasMedia, hasMedia,
onChange, onChange,
}: { }: {
labels: string[] labels: SelfLabel[]
hasMedia: boolean hasMedia: boolean
onChange: (v: string[]) => void onChange: (v: SelfLabel[]) => void
}) { }) {
const control = Dialog.useDialogControl() const control = Dialog.useDialogControl()
const t = useTheme() const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
const removeAdultLabel = () => { const hasLabel = labels.length > 0
const final = labels.filter(l => !ADULT_CONTENT_LABELS.includes(l))
onChange(final) const updateAdultLabels = (newLabels: AdultSelfLabel[]) => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) const newLabel = newLabels[newLabels.length - 1]
const filtered = labels.filter(l => !ADULT_CONTENT_LABELS.includes(l))
onChange([...filtered, newLabel].filter(Boolean) as SelfLabel[])
} }
const hasAdultSelection = const updateOtherLabels = (newLabels: OtherSelfLabel[]) => {
labels.includes('sexual') || const newLabel = newLabels[newLabels.length - 1]
labels.includes('nudity') || const filtered = labels.filter(l => !OTHER_SELF_LABELS.includes(l))
labels.includes('porn') onChange([...filtered, newLabel].filter(Boolean) as SelfLabel[])
}
if (!hasMedia && hasAdultSelection) { if (!hasMedia && hasLabel) {
removeAdultLabel() onChange([])
} }
return ( return (
@@ -64,10 +72,9 @@ export function LabelsBtn({
<Dialog.Handle /> <Dialog.Handle />
<DialogInner <DialogInner
labels={labels} labels={labels}
onChange={onChange}
hasAdultSelection={hasAdultSelection}
hasMedia={hasMedia} hasMedia={hasMedia}
removeAdultLabel={removeAdultLabel} updateAdultLabels={updateAdultLabels}
updateOtherLabels={updateOtherLabels}
/> />
</Dialog.Outer> </Dialog.Outer>
</> </>
@@ -76,16 +83,14 @@ export function LabelsBtn({
function DialogInner({ function DialogInner({
labels, labels,
onChange,
hasAdultSelection,
hasMedia, hasMedia,
removeAdultLabel, updateAdultLabels,
updateOtherLabels,
}: { }: {
labels: string[] labels: string[]
onChange: (v: string[]) => void
hasAdultSelection: boolean
hasMedia: boolean hasMedia: boolean
removeAdultLabel: () => void updateAdultLabels: (labels: AdultSelfLabel[]) => void
updateOtherLabels: (labels: OtherSelfLabel[]) => void
}) { }) {
const {_} = useLingui() const {_} = useLingui()
const control = Dialog.useDialogContext() const control = Dialog.useDialogContext()
@@ -95,104 +100,153 @@ function DialogInner({
<Dialog.ScrollableInner <Dialog.ScrollableInner
label={_(msg`Add a content warning`)} label={_(msg`Add a content warning`)}
style={[{maxWidth: 500}, a.w_full]}> style={[{maxWidth: 500}, a.w_full]}>
<View style={[a.flex_1, a.gap_md]}> <View style={[a.flex_1]}>
<Text style={[a.text_2xl, a.font_bold]}> <View style={[a.gap_sm]}>
<Trans>Add a content warning</Trans> <Text style={[a.text_2xl, a.font_bold]}>
</Text> <Trans>Add a content warning</Trans>
</Text>
<Text style={[t.atoms.text_contrast_medium, a.leading_snug]}>
{hasMedia ? (
<Trans>
Choose self-labels that are applicable for the media you are
posting. If none are selected, this post is suitable for all
audiences.
</Trans>
) : (
<Trans>
There are no self-labels that can be applied to this post.
</Trans>
)}
</Text>
</View>
<View <View style={[a.my_md, a.gap_lg]}>
style={[
a.border,
a.p_md,
t.atoms.border_contrast_high,
a.rounded_md,
]}>
<View
style={[a.flex_row, a.align_center, a.justify_between, a.pb_sm]}>
<Text style={[a.font_bold, a.text_lg]}>
<Trans>Adult Content</Trans>
</Text>
<Button
label={_(msg`Remove`)}
variant="ghost"
color="primary"
size="tiny"
onPress={removeAdultLabel}
disabled={!hasAdultSelection}
style={{opacity: hasAdultSelection ? 1 : 0}}
aria-hidden={!hasAdultSelection}>
<ButtonText>
<Trans>Remove</Trans>
</ButtonText>
</Button>
</View>
{hasMedia ? ( {hasMedia ? (
<> <>
<ToggleButton.Group <View>
label={_(msg`Adult Content labels`)} <View
values={labels} style={[
onChange={values => { a.flex_row,
onChange(values) a.align_center,
LayoutAnimation.configureNext( a.justify_between,
LayoutAnimation.Presets.easeInEaseOut, a.pb_sm,
) ]}>
}}> <Text style={[a.font_bold, a.text_lg]}>
<ToggleButton.Button name="sexual" label={_(msg`Suggestive`)}> <Trans>Adult Content</Trans>
<ToggleButton.ButtonText> </Text>
<Trans>Suggestive</Trans> </View>
</ToggleButton.ButtonText> <View
</ToggleButton.Button> style={[
<ToggleButton.Button name="nudity" label={_(msg`Nudity`)}> a.p_md,
<ToggleButton.ButtonText> a.rounded_sm,
<Trans>Nudity</Trans> a.border,
</ToggleButton.ButtonText> t.atoms.border_contrast_medium,
</ToggleButton.Button> ]}>
<ToggleButton.Button name="porn" label={_(msg`Porn`)}> <Toggle.Group
<ToggleButton.ButtonText> label={_(msg`Adult Content labels`)}
<Trans>Porn</Trans> values={labels}
</ToggleButton.ButtonText> onChange={values => {
</ToggleButton.Button> updateAdultLabels(values as AdultSelfLabel[])
</ToggleButton.Group> }}>
<View style={[a.gap_sm]}>
<Text style={[a.mt_sm, t.atoms.text_contrast_medium]}> <Toggle.Item name="sexual" label={_(msg`Suggestive`)}>
{labels.includes('sexual') ? ( <Toggle.Radio />
<Trans>Pictures meant for adults.</Trans> <Toggle.LabelText>
) : labels.includes('nudity') ? ( <Trans>Suggestive</Trans>
<Trans>Artistic or non-erotic nudity.</Trans> </Toggle.LabelText>
) : labels.includes('porn') ? ( </Toggle.Item>
<Trans>Sexual activity or erotic nudity.</Trans> <Toggle.Item name="nudity" label={_(msg`Nudity`)}>
) : ( <Toggle.Radio />
<Trans>If none are selected, suitable for all ages.</Trans> <Toggle.LabelText>
)} <Trans>Nudity</Trans>
</Text> </Toggle.LabelText>
</Toggle.Item>
<Toggle.Item name="porn" label={_(msg`Porn`)}>
<Toggle.Radio />
<Toggle.LabelText>
<Trans>Porn</Trans>
</Toggle.LabelText>
</Toggle.Item>
</View>
</Toggle.Group>
<Text style={[a.mt_sm, t.atoms.text_contrast_medium]}>
{labels.includes('sexual') ? (
<Trans>Pictures meant for adults.</Trans>
) : labels.includes('nudity') ? (
<Trans>Artistic or non-erotic nudity.</Trans>
) : labels.includes('porn') ? (
<Trans>Sexual activity or erotic nudity.</Trans>
) : (
<Trans>Does not contain adult content.</Trans>
)}
</Text>
</View>
</View>
<View>
<View
style={[
a.flex_row,
a.align_center,
a.justify_between,
a.pb_sm,
]}>
<Text style={[a.font_bold, a.text_lg]}>
<Trans>Other</Trans>
</Text>
</View>
<View
style={[
a.p_md,
a.rounded_sm,
a.border,
t.atoms.border_contrast_medium,
]}>
<Toggle.Group
label={_(msg`Adult Content labels`)}
values={labels}
onChange={values => {
updateOtherLabels(values as OtherSelfLabel[])
}}>
<Toggle.Item
name="graphic-media"
label={_(msg`Graphic Media`)}>
<Toggle.Checkbox />
<Toggle.LabelText>
<Trans>Graphic Media</Trans>
</Toggle.LabelText>
</Toggle.Item>
</Toggle.Group>
<Text style={[a.mt_sm, t.atoms.text_contrast_medium]}>
{labels.includes('graphic-media') ? (
<Trans>
Media that may be disturbing or inappropriate for some
audiences.
</Trans>
) : (
<Trans>
Does not contain graphic or disturbing content.
</Trans>
)}
</Text>
</View>
</View>
</> </>
) : ( ) : null}
<View>
<Text style={t.atoms.text_contrast_medium}>
<Trans>
<Text style={[a.font_bold, t.atoms.text_contrast_medium]}>
Not Applicable.
</Text>{' '}
This warning is only available for posts with media attached.
</Trans>
</Text>
</View>
)}
</View> </View>
</View> </View>
<Button <View style={[a.mt_sm]}>
label={_(msg`Done`)} <Button
onPress={() => control.close()} label={_(msg`Done`)}
color="primary" onPress={() => control.close()}
size="large" color="primary"
variant="solid" size={isWeb ? 'small' : 'large'}
style={a.mt_xl}> variant="solid">
<ButtonText> <ButtonText>
<Trans>Done</Trans> <Trans>Done</Trans>
</ButtonText> </ButtonText>
</Button> </Button>
</View>
</Dialog.ScrollableInner> </Dialog.ScrollableInner>
) )
} }
+3 -2
View File
@@ -1,6 +1,7 @@
import {ImagePickerAsset} from 'expo-image-picker' import {ImagePickerAsset} from 'expo-image-picker'
import {AppBskyFeedPostgate, RichText} from '@atproto/api' import {AppBskyFeedPostgate, RichText} from '@atproto/api'
import {SelfLabel} from '#/lib/moderation'
import {insertMentionAt} from '#/lib/strings/mention-manip' import {insertMentionAt} from '#/lib/strings/mention-manip'
import { import {
isBskyPostUrl, isBskyPostUrl,
@@ -48,7 +49,7 @@ export type EmbedDraft = {
export type ComposerDraft = { export type ComposerDraft = {
richtext: RichText richtext: RichText
labels: string[] labels: SelfLabel[]
postgate: AppBskyFeedPostgate.Record postgate: AppBskyFeedPostgate.Record
threadgate: ThreadgateAllowUISetting[] threadgate: ThreadgateAllowUISetting[]
embed: EmbedDraft embed: EmbedDraft
@@ -56,7 +57,7 @@ export type ComposerDraft = {
export type ComposerAction = export type ComposerAction =
| {type: 'update_richtext'; richtext: RichText} | {type: 'update_richtext'; richtext: RichText}
| {type: 'update_labels'; labels: string[]} | {type: 'update_labels'; labels: SelfLabel[]}
| {type: 'update_postgate'; postgate: AppBskyFeedPostgate.Record} | {type: 'update_postgate'; postgate: AppBskyFeedPostgate.Record}
| {type: 'update_threadgate'; threadgate: ThreadgateAllowUISetting[]} | {type: 'update_threadgate'; threadgate: ThreadgateAllowUISetting[]}
| {type: 'embed_add_images'; images: ComposerImage[]} | {type: 'embed_add_images'; images: ComposerImage[]}