diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/components/moderation/ContentHider.tsx | 68 | ||||
-rw-r--r-- | src/lib/moderation.ts | 8 | ||||
-rw-r--r-- | src/view/com/composer/labels/LabelsBtn.tsx | 288 | ||||
-rw-r--r-- | src/view/com/composer/state/composer.ts | 5 |
4 files changed, 247 insertions, 122 deletions
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 ( <View testID={testID} style={style}> @@ -99,8 +160,9 @@ export function ContentHider({ web({ marginBottom: 1, }), - ]}> - {desc.name} + ]} + numberOfLines={2}> + {labelName} </Text> {!modui.noOverride && ( <Text diff --git a/src/lib/moderation.ts b/src/lib/moderation.ts index 7576a9c33..be503f4c7 100644 --- a/src/lib/moderation.ts +++ b/src/lib/moderation.ts @@ -14,6 +14,14 @@ import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' 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( cause: ModerationCause | AppModerationCause, ): string { diff --git a/src/view/com/composer/labels/LabelsBtn.tsx b/src/view/com/composer/labels/LabelsBtn.tsx index d72366aea..a176426dc 100644 --- a/src/view/com/composer/labels/LabelsBtn.tsx +++ b/src/view/com/composer/labels/LabelsBtn.tsx @@ -1,44 +1,52 @@ import React from 'react' -import {Keyboard, LayoutAnimation, View} from 'react-native' +import {Keyboard, View} from 'react-native' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' 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 {Button, ButtonText} from '#/components/Button' 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 {Text} from '#/components/Typography' - -const ADULT_CONTENT_LABELS = ['sexual', 'nudity', 'porn'] - export function LabelsBtn({ labels, hasMedia, onChange, }: { - labels: string[] + labels: SelfLabel[] hasMedia: boolean - onChange: (v: string[]) => 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({ <Dialog.Handle /> <DialogInner labels={labels} - onChange={onChange} - hasAdultSelection={hasAdultSelection} hasMedia={hasMedia} - removeAdultLabel={removeAdultLabel} + updateAdultLabels={updateAdultLabels} + updateOtherLabels={updateOtherLabels} /> </Dialog.Outer> </> @@ -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({ <Dialog.ScrollableInner label={_(msg`Add a content warning`)} style={[{maxWidth: 500}, a.w_full]}> - <View style={[a.flex_1, a.gap_md]}> - <Text style={[a.text_2xl, a.font_bold]}> - <Trans>Add a content warning</Trans> - </Text> - - <View - 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> + <View style={[a.flex_1]}> + <View style={[a.gap_sm]}> + <Text style={[a.text_2xl, a.font_bold]}> + <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> - <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> + <View style={[a.my_md, a.gap_lg]}> {hasMedia ? ( <> - <ToggleButton.Group - label={_(msg`Adult Content labels`)} - values={labels} - onChange={values => { - onChange(values) - LayoutAnimation.configureNext( - LayoutAnimation.Presets.easeInEaseOut, - ) - }}> - <ToggleButton.Button name="sexual" label={_(msg`Suggestive`)}> - <ToggleButton.ButtonText> - <Trans>Suggestive</Trans> - </ToggleButton.ButtonText> - </ToggleButton.Button> - <ToggleButton.Button name="nudity" label={_(msg`Nudity`)}> - <ToggleButton.ButtonText> - <Trans>Nudity</Trans> - </ToggleButton.ButtonText> - </ToggleButton.Button> - <ToggleButton.Button name="porn" label={_(msg`Porn`)}> - <ToggleButton.ButtonText> - <Trans>Porn</Trans> - </ToggleButton.ButtonText> - </ToggleButton.Button> - </ToggleButton.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>If none are selected, suitable for all ages.</Trans> - )} - </Text> + <View> + <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> + </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 => { + updateAdultLabels(values as AdultSelfLabel[]) + }}> + <View style={[a.gap_sm]}> + <Toggle.Item name="sexual" label={_(msg`Suggestive`)}> + <Toggle.Radio /> + <Toggle.LabelText> + <Trans>Suggestive</Trans> + </Toggle.LabelText> + </Toggle.Item> + <Toggle.Item name="nudity" label={_(msg`Nudity`)}> + <Toggle.Radio /> + <Toggle.LabelText> + <Trans>Nudity</Trans> + </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> </> - ) : ( - <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> - )} + ) : null} </View> </View> - <Button - label={_(msg`Done`)} - onPress={() => control.close()} - color="primary" - size="large" - variant="solid" - style={a.mt_xl}> - <ButtonText> - <Trans>Done</Trans> - </ButtonText> - </Button> + <View style={[a.mt_sm]}> + <Button + label={_(msg`Done`)} + onPress={() => control.close()} + color="primary" + size={isWeb ? 'small' : 'large'} + variant="solid"> + <ButtonText> + <Trans>Done</Trans> + </ButtonText> + </Button> + </View> </Dialog.ScrollableInner> ) } 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[]} |