From 2e74f9839246c7dee718ac8bc12c22395ff002b5 Mon Sep 17 00:00:00 2001 From: Hailey Date: Wed, 16 Oct 2024 19:17:22 -0700 Subject: Add graphic media self label (#5758) Co-authored-by: Samuel Newman --- src/components/moderation/ContentHider.tsx | 68 ++++++- src/lib/moderation.ts | 8 + src/view/com/composer/labels/LabelsBtn.tsx | 288 +++++++++++++++++------------ src/view/com/composer/state/composer.ts | 5 +- 4 files changed, 247 insertions(+), 122 deletions(-) (limited to 'src') diff --git a/src/components/moderation/ContentHider.tsx b/src/components/moderation/ContentHider.tsx index bf9bae517..67aef67b4 100644 --- a/src/components/moderation/ContentHider.tsx +++ b/src/components/moderation/ContentHider.tsx @@ -4,9 +4,12 @@ import {ModerationUI} from '@atproto/api' import {msg, Trans} from '@lingui/macro' 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 {sanitizeDisplayName} from '#/lib/strings/display-names' +import {useLabelDefinitions} from '#/state/preferences' import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' import {Button} from '#/components/Button' import { @@ -34,10 +37,68 @@ export function ContentHider({ const {gtMobile} = useBreakpoints() const [override, setOverride] = React.useState(false) const control = useModerationDetailsDialogControl() + const {labelDefs} = useLabelDefinitions() + const globalLabelStrings = useGlobalLabelStrings() + const {i18n} = useLingui() const blur = modui?.blurs[0] 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))) { return ( @@ -99,8 +160,9 @@ export function ContentHider({ web({ marginBottom: 1, }), - ]}> - {desc.name} + ]} + numberOfLines={2}> + {labelName} {!modui.noOverride && ( void + onChange: (v: SelfLabel[]) => void }) { const control = Dialog.useDialogControl() const t = useTheme() const {_} = useLingui() - const removeAdultLabel = () => { - const final = labels.filter(l => !ADULT_CONTENT_LABELS.includes(l)) - onChange(final) - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) + const hasLabel = labels.length > 0 + + const updateAdultLabels = (newLabels: AdultSelfLabel[]) => { + 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 = - labels.includes('sexual') || - labels.includes('nudity') || - labels.includes('porn') + const updateOtherLabels = (newLabels: OtherSelfLabel[]) => { + const newLabel = newLabels[newLabels.length - 1] + const filtered = labels.filter(l => !OTHER_SELF_LABELS.includes(l)) + onChange([...filtered, newLabel].filter(Boolean) as SelfLabel[]) + } - if (!hasMedia && hasAdultSelection) { - removeAdultLabel() + if (!hasMedia && hasLabel) { + onChange([]) } return ( @@ -64,10 +72,9 @@ export function LabelsBtn({ @@ -76,16 +83,14 @@ export function LabelsBtn({ function DialogInner({ labels, - onChange, - hasAdultSelection, hasMedia, - removeAdultLabel, + updateAdultLabels, + updateOtherLabels, }: { labels: string[] - onChange: (v: string[]) => void - hasAdultSelection: boolean hasMedia: boolean - removeAdultLabel: () => void + updateAdultLabels: (labels: AdultSelfLabel[]) => void + updateOtherLabels: (labels: OtherSelfLabel[]) => void }) { const {_} = useLingui() const control = Dialog.useDialogContext() @@ -95,104 +100,153 @@ function DialogInner({ - - - Add a content warning - - - - - - Adult Content - + + + + Add a content warning + + + {hasMedia ? ( + + Choose self-labels that are applicable for the media you are + posting. If none are selected, this post is suitable for all + audiences. + + ) : ( + + There are no self-labels that can be applied to this post. + + )} + + - - + {hasMedia ? ( <> - { - onChange(values) - LayoutAnimation.configureNext( - LayoutAnimation.Presets.easeInEaseOut, - ) - }}> - - - Suggestive - - - - - Nudity - - - - - Porn - - - - - - {labels.includes('sexual') ? ( - Pictures meant for adults. - ) : labels.includes('nudity') ? ( - Artistic or non-erotic nudity. - ) : labels.includes('porn') ? ( - Sexual activity or erotic nudity. - ) : ( - If none are selected, suitable for all ages. - )} - + + + + Adult Content + + + + { + updateAdultLabels(values as AdultSelfLabel[]) + }}> + + + + + Suggestive + + + + + + Nudity + + + + + + Porn + + + + + + {labels.includes('sexual') ? ( + Pictures meant for adults. + ) : labels.includes('nudity') ? ( + Artistic or non-erotic nudity. + ) : labels.includes('porn') ? ( + Sexual activity or erotic nudity. + ) : ( + Does not contain adult content. + )} + + + + + + + Other + + + + { + updateOtherLabels(values as OtherSelfLabel[]) + }}> + + + + Graphic Media + + + + + {labels.includes('graphic-media') ? ( + + Media that may be disturbing or inappropriate for some + audiences. + + ) : ( + + Does not contain graphic or disturbing content. + + )} + + + - ) : ( - - - - - Not Applicable. - {' '} - This warning is only available for posts with media attached. - - - - )} + ) : null} - + + + ) } diff --git a/src/view/com/composer/state/composer.ts b/src/view/com/composer/state/composer.ts index e37690342..049488f3a 100644 --- a/src/view/com/composer/state/composer.ts +++ b/src/view/com/composer/state/composer.ts @@ -1,6 +1,7 @@ import {ImagePickerAsset} from 'expo-image-picker' import {AppBskyFeedPostgate, RichText} from '@atproto/api' +import {SelfLabel} from '#/lib/moderation' import {insertMentionAt} from '#/lib/strings/mention-manip' import { isBskyPostUrl, @@ -48,7 +49,7 @@ export type EmbedDraft = { export type ComposerDraft = { richtext: RichText - labels: string[] + labels: SelfLabel[] postgate: AppBskyFeedPostgate.Record threadgate: ThreadgateAllowUISetting[] embed: EmbedDraft @@ -56,7 +57,7 @@ export type ComposerDraft = { export type ComposerAction = | {type: 'update_richtext'; richtext: RichText} - | {type: 'update_labels'; labels: string[]} + | {type: 'update_labels'; labels: SelfLabel[]} | {type: 'update_postgate'; postgate: AppBskyFeedPostgate.Record} | {type: 'update_threadgate'; threadgate: ThreadgateAllowUISetting[]} | {type: 'embed_add_images'; images: ComposerImage[]} -- cgit 1.4.1