Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add graphic media self label (#5758)

Co-authored-by: Samuel Newman <mozzius@protonmail.com>

authored by

Hailey
Samuel Newman
and committed by
GitHub
2e74f983 3d9663db

+247 -122
+65 -3
src/components/moderation/ContentHider.tsx
··· 4 4 import {msg, Trans} from '@lingui/macro' 5 5 import {useLingui} from '@lingui/react' 6 6 7 - import {isJustAMute} from '#/lib/moderation' 7 + import {ADULT_CONTENT_LABELS, isJustAMute} from '#/lib/moderation' 8 + import {useGlobalLabelStrings} from '#/lib/moderation/useGlobalLabelStrings' 9 + import {getDefinition, getLabelStrings} from '#/lib/moderation/useLabelInfo' 8 10 import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription' 9 11 import {sanitizeDisplayName} from '#/lib/strings/display-names' 12 + import {useLabelDefinitions} from '#/state/preferences' 10 13 import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' 11 14 import {Button} from '#/components/Button' 12 15 import { ··· 34 37 const {gtMobile} = useBreakpoints() 35 38 const [override, setOverride] = React.useState(false) 36 39 const control = useModerationDetailsDialogControl() 40 + const {labelDefs} = useLabelDefinitions() 41 + const globalLabelStrings = useGlobalLabelStrings() 42 + const {i18n} = useLingui() 37 43 38 44 const blur = modui?.blurs[0] 39 45 const desc = useModerationCauseDescription(blur) 40 46 47 + const labelName = React.useMemo(() => { 48 + if (!modui?.blurs || !blur) { 49 + return undefined 50 + } 51 + if ( 52 + blur.type !== 'label' || 53 + (blur.type === 'label' && blur.source.type !== 'user') 54 + ) { 55 + return desc.name 56 + } 57 + 58 + let hasAdultContentLabel = false 59 + const selfBlurNames = modui.blurs 60 + .filter(cause => { 61 + if (cause.type !== 'label') { 62 + return false 63 + } 64 + if (cause.source.type !== 'user') { 65 + return false 66 + } 67 + if (ADULT_CONTENT_LABELS.includes(cause.label.val)) { 68 + if (hasAdultContentLabel) { 69 + return false 70 + } 71 + hasAdultContentLabel = true 72 + } 73 + return true 74 + }) 75 + .slice(0, 2) 76 + .map(cause => { 77 + if (cause.type !== 'label') { 78 + return 79 + } 80 + 81 + const def = cause.labelDef || getDefinition(labelDefs, cause.label) 82 + if (def.identifier === 'porn' || def.identifier === 'sexual') { 83 + return _(msg`Adult Content`) 84 + } 85 + return getLabelStrings(i18n.locale, globalLabelStrings, def).name 86 + }) 87 + 88 + if (selfBlurNames.length === 0) { 89 + return desc.name 90 + } 91 + return [...new Set(selfBlurNames)].join(', ') 92 + }, [ 93 + _, 94 + modui?.blurs, 95 + blur, 96 + desc.name, 97 + labelDefs, 98 + i18n.locale, 99 + globalLabelStrings, 100 + ]) 101 + 41 102 if (!blur || (ignoreMute && isJustAMute(modui))) { 42 103 return ( 43 104 <View testID={testID} style={style}> ··· 99 160 web({ 100 161 marginBottom: 1, 101 162 }), 102 - ]}> 103 - {desc.name} 163 + ]} 164 + numberOfLines={2}> 165 + {labelName} 104 166 </Text> 105 167 {!modui.noOverride && ( 106 168 <Text
+8
src/lib/moderation.ts
··· 14 14 import {sanitizeHandle} from '#/lib/strings/handles' 15 15 import {AppModerationCause} from '#/components/Pills' 16 16 17 + export const ADULT_CONTENT_LABELS = ['sexual', 'nudity', 'porn'] 18 + export const OTHER_SELF_LABELS = ['graphic-media'] 19 + export const SELF_LABELS = [...ADULT_CONTENT_LABELS, ...OTHER_SELF_LABELS] 20 + 21 + export type AdultSelfLabel = (typeof ADULT_CONTENT_LABELS)[number] 22 + export type OtherSelfLabel = (typeof OTHER_SELF_LABELS)[number] 23 + export type SelfLabel = (typeof SELF_LABELS)[number] 24 + 17 25 export function getModerationCauseKey( 18 26 cause: ModerationCause | AppModerationCause, 19 27 ): string {
+171 -117
src/view/com/composer/labels/LabelsBtn.tsx
··· 1 1 import React from 'react' 2 - import {Keyboard, LayoutAnimation, View} from 'react-native' 2 + import {Keyboard, View} from 'react-native' 3 3 import {msg, Trans} from '@lingui/macro' 4 4 import {useLingui} from '@lingui/react' 5 5 6 6 import {ShieldExclamation} from '#/lib/icons' 7 + import { 8 + ADULT_CONTENT_LABELS, 9 + AdultSelfLabel, 10 + OTHER_SELF_LABELS, 11 + OtherSelfLabel, 12 + SelfLabel, 13 + } from '#/lib/moderation' 14 + import {isWeb} from '#/platform/detection' 7 15 import {atoms as a, useTheme} from '#/alf' 8 16 import {Button, ButtonText} from '#/components/Button' 9 17 import * as Dialog from '#/components/Dialog' 10 - import * as ToggleButton from '#/components/forms/ToggleButton' 18 + import * as Toggle from '#/components/forms/Toggle' 11 19 import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' 12 20 import {Text} from '#/components/Typography' 13 - 14 - const ADULT_CONTENT_LABELS = ['sexual', 'nudity', 'porn'] 15 - 16 21 export function LabelsBtn({ 17 22 labels, 18 23 hasMedia, 19 24 onChange, 20 25 }: { 21 - labels: string[] 26 + labels: SelfLabel[] 22 27 hasMedia: boolean 23 - onChange: (v: string[]) => void 28 + onChange: (v: SelfLabel[]) => void 24 29 }) { 25 30 const control = Dialog.useDialogControl() 26 31 const t = useTheme() 27 32 const {_} = useLingui() 28 33 29 - const removeAdultLabel = () => { 30 - const final = labels.filter(l => !ADULT_CONTENT_LABELS.includes(l)) 31 - onChange(final) 32 - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) 34 + const hasLabel = labels.length > 0 35 + 36 + const updateAdultLabels = (newLabels: AdultSelfLabel[]) => { 37 + const newLabel = newLabels[newLabels.length - 1] 38 + const filtered = labels.filter(l => !ADULT_CONTENT_LABELS.includes(l)) 39 + onChange([...filtered, newLabel].filter(Boolean) as SelfLabel[]) 33 40 } 34 41 35 - const hasAdultSelection = 36 - labels.includes('sexual') || 37 - labels.includes('nudity') || 38 - labels.includes('porn') 42 + const updateOtherLabels = (newLabels: OtherSelfLabel[]) => { 43 + const newLabel = newLabels[newLabels.length - 1] 44 + const filtered = labels.filter(l => !OTHER_SELF_LABELS.includes(l)) 45 + onChange([...filtered, newLabel].filter(Boolean) as SelfLabel[]) 46 + } 39 47 40 - if (!hasMedia && hasAdultSelection) { 41 - removeAdultLabel() 48 + if (!hasMedia && hasLabel) { 49 + onChange([]) 42 50 } 43 51 44 52 return ( ··· 64 72 <Dialog.Handle /> 65 73 <DialogInner 66 74 labels={labels} 67 - onChange={onChange} 68 - hasAdultSelection={hasAdultSelection} 69 75 hasMedia={hasMedia} 70 - removeAdultLabel={removeAdultLabel} 76 + updateAdultLabels={updateAdultLabels} 77 + updateOtherLabels={updateOtherLabels} 71 78 /> 72 79 </Dialog.Outer> 73 80 </> ··· 76 83 77 84 function DialogInner({ 78 85 labels, 79 - onChange, 80 - hasAdultSelection, 81 86 hasMedia, 82 - removeAdultLabel, 87 + updateAdultLabels, 88 + updateOtherLabels, 83 89 }: { 84 90 labels: string[] 85 - onChange: (v: string[]) => void 86 - hasAdultSelection: boolean 87 91 hasMedia: boolean 88 - removeAdultLabel: () => void 92 + updateAdultLabels: (labels: AdultSelfLabel[]) => void 93 + updateOtherLabels: (labels: OtherSelfLabel[]) => void 89 94 }) { 90 95 const {_} = useLingui() 91 96 const control = Dialog.useDialogContext() ··· 95 100 <Dialog.ScrollableInner 96 101 label={_(msg`Add a content warning`)} 97 102 style={[{maxWidth: 500}, a.w_full]}> 98 - <View style={[a.flex_1, a.gap_md]}> 99 - <Text style={[a.text_2xl, a.font_bold]}> 100 - <Trans>Add a content warning</Trans> 101 - </Text> 102 - 103 - <View 104 - style={[ 105 - a.border, 106 - a.p_md, 107 - t.atoms.border_contrast_high, 108 - a.rounded_md, 109 - ]}> 110 - <View 111 - style={[a.flex_row, a.align_center, a.justify_between, a.pb_sm]}> 112 - <Text style={[a.font_bold, a.text_lg]}> 113 - <Trans>Adult Content</Trans> 114 - </Text> 103 + <View style={[a.flex_1]}> 104 + <View style={[a.gap_sm]}> 105 + <Text style={[a.text_2xl, a.font_bold]}> 106 + <Trans>Add a content warning</Trans> 107 + </Text> 108 + <Text style={[t.atoms.text_contrast_medium, a.leading_snug]}> 109 + {hasMedia ? ( 110 + <Trans> 111 + Choose self-labels that are applicable for the media you are 112 + posting. If none are selected, this post is suitable for all 113 + audiences. 114 + </Trans> 115 + ) : ( 116 + <Trans> 117 + There are no self-labels that can be applied to this post. 118 + </Trans> 119 + )} 120 + </Text> 121 + </View> 115 122 116 - <Button 117 - label={_(msg`Remove`)} 118 - variant="ghost" 119 - color="primary" 120 - size="tiny" 121 - onPress={removeAdultLabel} 122 - disabled={!hasAdultSelection} 123 - style={{opacity: hasAdultSelection ? 1 : 0}} 124 - aria-hidden={!hasAdultSelection}> 125 - <ButtonText> 126 - <Trans>Remove</Trans> 127 - </ButtonText> 128 - </Button> 129 - </View> 123 + <View style={[a.my_md, a.gap_lg]}> 130 124 {hasMedia ? ( 131 125 <> 132 - <ToggleButton.Group 133 - label={_(msg`Adult Content labels`)} 134 - values={labels} 135 - onChange={values => { 136 - onChange(values) 137 - LayoutAnimation.configureNext( 138 - LayoutAnimation.Presets.easeInEaseOut, 139 - ) 140 - }}> 141 - <ToggleButton.Button name="sexual" label={_(msg`Suggestive`)}> 142 - <ToggleButton.ButtonText> 143 - <Trans>Suggestive</Trans> 144 - </ToggleButton.ButtonText> 145 - </ToggleButton.Button> 146 - <ToggleButton.Button name="nudity" label={_(msg`Nudity`)}> 147 - <ToggleButton.ButtonText> 148 - <Trans>Nudity</Trans> 149 - </ToggleButton.ButtonText> 150 - </ToggleButton.Button> 151 - <ToggleButton.Button name="porn" label={_(msg`Porn`)}> 152 - <ToggleButton.ButtonText> 153 - <Trans>Porn</Trans> 154 - </ToggleButton.ButtonText> 155 - </ToggleButton.Button> 156 - </ToggleButton.Group> 157 - 158 - <Text style={[a.mt_sm, t.atoms.text_contrast_medium]}> 159 - {labels.includes('sexual') ? ( 160 - <Trans>Pictures meant for adults.</Trans> 161 - ) : labels.includes('nudity') ? ( 162 - <Trans>Artistic or non-erotic nudity.</Trans> 163 - ) : labels.includes('porn') ? ( 164 - <Trans>Sexual activity or erotic nudity.</Trans> 165 - ) : ( 166 - <Trans>If none are selected, suitable for all ages.</Trans> 167 - )} 168 - </Text> 126 + <View> 127 + <View 128 + style={[ 129 + a.flex_row, 130 + a.align_center, 131 + a.justify_between, 132 + a.pb_sm, 133 + ]}> 134 + <Text style={[a.font_bold, a.text_lg]}> 135 + <Trans>Adult Content</Trans> 136 + </Text> 137 + </View> 138 + <View 139 + style={[ 140 + a.p_md, 141 + a.rounded_sm, 142 + a.border, 143 + t.atoms.border_contrast_medium, 144 + ]}> 145 + <Toggle.Group 146 + label={_(msg`Adult Content labels`)} 147 + values={labels} 148 + onChange={values => { 149 + updateAdultLabels(values as AdultSelfLabel[]) 150 + }}> 151 + <View style={[a.gap_sm]}> 152 + <Toggle.Item name="sexual" label={_(msg`Suggestive`)}> 153 + <Toggle.Radio /> 154 + <Toggle.LabelText> 155 + <Trans>Suggestive</Trans> 156 + </Toggle.LabelText> 157 + </Toggle.Item> 158 + <Toggle.Item name="nudity" label={_(msg`Nudity`)}> 159 + <Toggle.Radio /> 160 + <Toggle.LabelText> 161 + <Trans>Nudity</Trans> 162 + </Toggle.LabelText> 163 + </Toggle.Item> 164 + <Toggle.Item name="porn" label={_(msg`Porn`)}> 165 + <Toggle.Radio /> 166 + <Toggle.LabelText> 167 + <Trans>Porn</Trans> 168 + </Toggle.LabelText> 169 + </Toggle.Item> 170 + </View> 171 + </Toggle.Group> 172 + <Text style={[a.mt_sm, t.atoms.text_contrast_medium]}> 173 + {labels.includes('sexual') ? ( 174 + <Trans>Pictures meant for adults.</Trans> 175 + ) : labels.includes('nudity') ? ( 176 + <Trans>Artistic or non-erotic nudity.</Trans> 177 + ) : labels.includes('porn') ? ( 178 + <Trans>Sexual activity or erotic nudity.</Trans> 179 + ) : ( 180 + <Trans>Does not contain adult content.</Trans> 181 + )} 182 + </Text> 183 + </View> 184 + </View> 185 + <View> 186 + <View 187 + style={[ 188 + a.flex_row, 189 + a.align_center, 190 + a.justify_between, 191 + a.pb_sm, 192 + ]}> 193 + <Text style={[a.font_bold, a.text_lg]}> 194 + <Trans>Other</Trans> 195 + </Text> 196 + </View> 197 + <View 198 + style={[ 199 + a.p_md, 200 + a.rounded_sm, 201 + a.border, 202 + t.atoms.border_contrast_medium, 203 + ]}> 204 + <Toggle.Group 205 + label={_(msg`Adult Content labels`)} 206 + values={labels} 207 + onChange={values => { 208 + updateOtherLabels(values as OtherSelfLabel[]) 209 + }}> 210 + <Toggle.Item 211 + name="graphic-media" 212 + label={_(msg`Graphic Media`)}> 213 + <Toggle.Checkbox /> 214 + <Toggle.LabelText> 215 + <Trans>Graphic Media</Trans> 216 + </Toggle.LabelText> 217 + </Toggle.Item> 218 + </Toggle.Group> 219 + <Text style={[a.mt_sm, t.atoms.text_contrast_medium]}> 220 + {labels.includes('graphic-media') ? ( 221 + <Trans> 222 + Media that may be disturbing or inappropriate for some 223 + audiences. 224 + </Trans> 225 + ) : ( 226 + <Trans> 227 + Does not contain graphic or disturbing content. 228 + </Trans> 229 + )} 230 + </Text> 231 + </View> 232 + </View> 169 233 </> 170 - ) : ( 171 - <View> 172 - <Text style={t.atoms.text_contrast_medium}> 173 - <Trans> 174 - <Text style={[a.font_bold, t.atoms.text_contrast_medium]}> 175 - Not Applicable. 176 - </Text>{' '} 177 - This warning is only available for posts with media attached. 178 - </Trans> 179 - </Text> 180 - </View> 181 - )} 234 + ) : null} 182 235 </View> 183 236 </View> 184 237 185 - <Button 186 - label={_(msg`Done`)} 187 - onPress={() => control.close()} 188 - color="primary" 189 - size="large" 190 - variant="solid" 191 - style={a.mt_xl}> 192 - <ButtonText> 193 - <Trans>Done</Trans> 194 - </ButtonText> 195 - </Button> 238 + <View style={[a.mt_sm]}> 239 + <Button 240 + label={_(msg`Done`)} 241 + onPress={() => control.close()} 242 + color="primary" 243 + size={isWeb ? 'small' : 'large'} 244 + variant="solid"> 245 + <ButtonText> 246 + <Trans>Done</Trans> 247 + </ButtonText> 248 + </Button> 249 + </View> 196 250 </Dialog.ScrollableInner> 197 251 ) 198 252 }
+3 -2
src/view/com/composer/state/composer.ts
··· 1 1 import {ImagePickerAsset} from 'expo-image-picker' 2 2 import {AppBskyFeedPostgate, RichText} from '@atproto/api' 3 3 4 + import {SelfLabel} from '#/lib/moderation' 4 5 import {insertMentionAt} from '#/lib/strings/mention-manip' 5 6 import { 6 7 isBskyPostUrl, ··· 48 49 49 50 export type ComposerDraft = { 50 51 richtext: RichText 51 - labels: string[] 52 + labels: SelfLabel[] 52 53 postgate: AppBskyFeedPostgate.Record 53 54 threadgate: ThreadgateAllowUISetting[] 54 55 embed: EmbedDraft ··· 56 57 57 58 export type ComposerAction = 58 59 | {type: 'update_richtext'; richtext: RichText} 59 - | {type: 'update_labels'; labels: string[]} 60 + | {type: 'update_labels'; labels: SelfLabel[]} 60 61 | {type: 'update_postgate'; postgate: AppBskyFeedPostgate.Record} 61 62 | {type: 'update_threadgate'; threadgate: ThreadgateAllowUISetting[]} 62 63 | {type: 'embed_add_images'; images: ComposerImage[]}