mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
0
fork

Configure Feed

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

at statsig-proxy 554 lines 17 kB view raw
1import React from 'react' 2import {View} from 'react-native' 3import {useSafeAreaFrame} from 'react-native-safe-area-context' 4import {ComAtprotoLabelDefs} from '@atproto/api' 5import {LABELS} from '@atproto/api' 6import {msg, Trans} from '@lingui/macro' 7import {useLingui} from '@lingui/react' 8import {useFocusEffect} from '@react-navigation/native' 9 10import {getLabelingServiceTitle} from '#/lib/moderation' 11import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' 12import {logger} from '#/logger' 13import { 14 useMyLabelersQuery, 15 usePreferencesQuery, 16 UsePreferencesQueryResponse, 17 usePreferencesSetAdultContentMutation, 18} from '#/state/queries/preferences' 19import { 20 useProfileQuery, 21 useProfileUpdateMutation, 22} from '#/state/queries/profile' 23import {useSession} from '#/state/session' 24import {useSetMinimalShellMode} from '#/state/shell' 25import {useAnalytics} from 'lib/analytics/analytics' 26import {ViewHeader} from '#/view/com/util/ViewHeader' 27import {CenteredView} from '#/view/com/util/Views' 28import {ScrollView} from '#/view/com/util/Views' 29import {atoms as a, useBreakpoints, useTheme, ViewStyleProp} from '#/alf' 30import {Button, ButtonText} from '#/components/Button' 31import * as Dialog from '#/components/Dialog' 32import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings' 33import {useGlobalDialogsControlContext} from '#/components/dialogs/Context' 34import {Divider} from '#/components/Divider' 35import * as Toggle from '#/components/forms/Toggle' 36import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron' 37import {CircleBanSign_Stroke2_Corner0_Rounded as CircleBanSign} from '#/components/icons/CircleBanSign' 38import {Props as SVGIconProps} from '#/components/icons/common' 39import {Filter_Stroke2_Corner0_Rounded as Filter} from '#/components/icons/Filter' 40import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group' 41import {Person_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person' 42import * as LabelingService from '#/components/LabelingServiceCard' 43import {InlineLink, Link} from '#/components/Link' 44import {Loader} from '#/components/Loader' 45import {GlobalLabelPreference} from '#/components/moderation/LabelPreference' 46import {Text} from '#/components/Typography' 47 48function ErrorState({error}: {error: string}) { 49 const t = useTheme() 50 return ( 51 <View style={[a.p_xl]}> 52 <Text 53 style={[ 54 a.text_md, 55 a.leading_normal, 56 a.pb_md, 57 t.atoms.text_contrast_medium, 58 ]}> 59 <Trans> 60 Hmmmm, it seems we're having trouble loading this data. See below for 61 more details. If this issue persists, please contact us. 62 </Trans> 63 </Text> 64 <View 65 style={[ 66 a.relative, 67 a.py_md, 68 a.px_lg, 69 a.rounded_md, 70 a.mb_2xl, 71 t.atoms.bg_contrast_25, 72 ]}> 73 <Text style={[a.text_md, a.leading_normal]}>{error}</Text> 74 </View> 75 </View> 76 ) 77} 78 79export function ModerationScreen( 80 _props: NativeStackScreenProps<CommonNavigatorParams, 'Moderation'>, 81) { 82 const t = useTheme() 83 const {_} = useLingui() 84 const { 85 isLoading: isPreferencesLoading, 86 error: preferencesError, 87 data: preferences, 88 } = usePreferencesQuery() 89 const {gtMobile} = useBreakpoints() 90 const {height} = useSafeAreaFrame() 91 92 const isLoading = isPreferencesLoading 93 const error = preferencesError 94 95 return ( 96 <CenteredView 97 testID="moderationScreen" 98 style={[ 99 t.atoms.border_contrast_low, 100 t.atoms.bg, 101 {minHeight: height}, 102 ...(gtMobile ? [a.border_l, a.border_r] : []), 103 ]}> 104 <ViewHeader title={_(msg`Moderation`)} showOnDesktop /> 105 106 {isLoading ? ( 107 <View style={[a.w_full, a.align_center, a.pt_2xl]}> 108 <Loader size="xl" fill={t.atoms.text.color} /> 109 </View> 110 ) : error || !preferences ? ( 111 <ErrorState 112 error={ 113 preferencesError?.toString() || 114 _(msg`Something went wrong, please try again.`) 115 } 116 /> 117 ) : ( 118 <ModerationScreenInner preferences={preferences} /> 119 )} 120 </CenteredView> 121 ) 122} 123 124function SubItem({ 125 title, 126 icon: Icon, 127 style, 128}: ViewStyleProp & { 129 title: string 130 icon: React.ComponentType<SVGIconProps> 131}) { 132 const t = useTheme() 133 return ( 134 <View 135 style={[ 136 a.w_full, 137 a.flex_row, 138 a.align_center, 139 a.justify_between, 140 a.p_lg, 141 a.gap_sm, 142 style, 143 ]}> 144 <View style={[a.flex_row, a.align_center, a.gap_md]}> 145 <Icon size="md" style={[t.atoms.text_contrast_medium]} /> 146 <Text style={[a.text_sm, a.font_bold]}>{title}</Text> 147 </View> 148 <ChevronRight 149 size="sm" 150 style={[t.atoms.text_contrast_low, a.self_end, {paddingBottom: 2}]} 151 /> 152 </View> 153 ) 154} 155 156export function ModerationScreenInner({ 157 preferences, 158}: { 159 preferences: UsePreferencesQueryResponse 160}) { 161 const {_} = useLingui() 162 const t = useTheme() 163 const setMinimalShellMode = useSetMinimalShellMode() 164 const {screen} = useAnalytics() 165 const {gtMobile} = useBreakpoints() 166 const {mutedWordsDialogControl} = useGlobalDialogsControlContext() 167 const birthdateDialogControl = Dialog.useDialogControl() 168 const { 169 isLoading: isLabelersLoading, 170 data: labelers, 171 error: labelersError, 172 } = useMyLabelersQuery() 173 174 useFocusEffect( 175 React.useCallback(() => { 176 screen('Moderation') 177 setMinimalShellMode(false) 178 }, [screen, setMinimalShellMode]), 179 ) 180 181 const {mutateAsync: setAdultContentPref, variables: optimisticAdultContent} = 182 usePreferencesSetAdultContentMutation() 183 const adultContentEnabled = !!( 184 (optimisticAdultContent && optimisticAdultContent.enabled) || 185 (!optimisticAdultContent && preferences.moderationPrefs.adultContentEnabled) 186 ) 187 const ageNotSet = !preferences.userAge 188 const isUnderage = (preferences.userAge || 0) < 18 189 190 const onToggleAdultContentEnabled = React.useCallback( 191 async (selected: boolean) => { 192 try { 193 await setAdultContentPref({ 194 enabled: selected, 195 }) 196 } catch (e: any) { 197 logger.error(`Failed to set adult content pref`, { 198 message: e.message, 199 }) 200 } 201 }, 202 [setAdultContentPref], 203 ) 204 205 return ( 206 <ScrollView 207 contentContainerStyle={[ 208 a.border_0, 209 a.pt_2xl, 210 a.px_lg, 211 gtMobile && a.px_2xl, 212 ]}> 213 <Text 214 style={[a.text_md, a.font_bold, a.pb_md, t.atoms.text_contrast_high]}> 215 <Trans>Moderation tools</Trans> 216 </Text> 217 218 <View 219 style={[ 220 a.w_full, 221 a.rounded_md, 222 a.overflow_hidden, 223 t.atoms.bg_contrast_25, 224 ]}> 225 <Button 226 testID="mutedWordsBtn" 227 label={_(msg`Open muted words and tags settings`)} 228 onPress={() => mutedWordsDialogControl.open()}> 229 {state => ( 230 <SubItem 231 title={_(msg`Muted words & tags`)} 232 icon={Filter} 233 style={[ 234 (state.hovered || state.pressed) && [t.atoms.bg_contrast_50], 235 ]} 236 /> 237 )} 238 </Button> 239 <Divider /> 240 <Link testID="moderationlistsBtn" to="/moderation/modlists"> 241 {state => ( 242 <SubItem 243 title={_(msg`Moderation lists`)} 244 icon={Group} 245 style={[ 246 (state.hovered || state.pressed) && [t.atoms.bg_contrast_50], 247 ]} 248 /> 249 )} 250 </Link> 251 <Divider /> 252 <Link testID="mutedAccountsBtn" to="/moderation/muted-accounts"> 253 {state => ( 254 <SubItem 255 title={_(msg`Muted accounts`)} 256 icon={Person} 257 style={[ 258 (state.hovered || state.pressed) && [t.atoms.bg_contrast_50], 259 ]} 260 /> 261 )} 262 </Link> 263 <Divider /> 264 <Link testID="blockedAccountsBtn" to="/moderation/blocked-accounts"> 265 {state => ( 266 <SubItem 267 title={_(msg`Blocked accounts`)} 268 icon={CircleBanSign} 269 style={[ 270 (state.hovered || state.pressed) && [t.atoms.bg_contrast_50], 271 ]} 272 /> 273 )} 274 </Link> 275 </View> 276 277 <Text 278 style={[ 279 a.pt_2xl, 280 a.pb_md, 281 a.text_md, 282 a.font_bold, 283 t.atoms.text_contrast_high, 284 ]}> 285 <Trans>Content filters</Trans> 286 </Text> 287 288 <View style={[a.gap_md]}> 289 {ageNotSet && ( 290 <> 291 <Button 292 label={_(msg`Confirm your birthdate`)} 293 size="small" 294 variant="solid" 295 color="secondary" 296 onPress={() => { 297 birthdateDialogControl.open() 298 }} 299 style={[a.justify_between, a.rounded_md, a.px_lg, a.py_lg]}> 300 <ButtonText> 301 <Trans>Confirm your age:</Trans> 302 </ButtonText> 303 <ButtonText> 304 <Trans>Set birthdate</Trans> 305 </ButtonText> 306 </Button> 307 308 <BirthDateSettingsDialog control={birthdateDialogControl} /> 309 </> 310 )} 311 <View 312 style={[ 313 a.w_full, 314 a.rounded_md, 315 a.overflow_hidden, 316 t.atoms.bg_contrast_25, 317 ]}> 318 {!ageNotSet && !isUnderage && ( 319 <> 320 <View 321 style={[ 322 a.py_lg, 323 a.px_lg, 324 a.flex_row, 325 a.align_center, 326 a.justify_between, 327 ]}> 328 <Text style={[a.font_semibold, t.atoms.text_contrast_high]}> 329 <Trans>Enable adult content</Trans> 330 </Text> 331 <Toggle.Item 332 label={_(msg`Toggle to enable or disable adult content`)} 333 name="adultContent" 334 value={adultContentEnabled} 335 onChange={onToggleAdultContentEnabled}> 336 <View style={[a.flex_row, a.align_center, a.gap_sm]}> 337 <Text style={[t.atoms.text_contrast_medium]}> 338 {adultContentEnabled ? ( 339 <Trans>Enabled</Trans> 340 ) : ( 341 <Trans>Disabled</Trans> 342 )} 343 </Text> 344 <Toggle.Switch /> 345 </View> 346 </Toggle.Item> 347 </View> 348 <Divider /> 349 </> 350 )} 351 {!isUnderage && adultContentEnabled && ( 352 <> 353 <GlobalLabelPreference labelDefinition={LABELS.porn} /> 354 <Divider /> 355 <GlobalLabelPreference labelDefinition={LABELS.sexual} /> 356 <Divider /> 357 <GlobalLabelPreference 358 labelDefinition={LABELS['graphic-media']} 359 /> 360 <Divider /> 361 </> 362 )} 363 <GlobalLabelPreference labelDefinition={LABELS.nudity} /> 364 </View> 365 </View> 366 367 <Text 368 style={[ 369 a.text_md, 370 a.font_bold, 371 a.pt_2xl, 372 a.pb_md, 373 t.atoms.text_contrast_high, 374 ]}> 375 <Trans>Advanced</Trans> 376 </Text> 377 378 {isLabelersLoading ? ( 379 <View style={[a.w_full, a.align_center, a.p_lg]}> 380 <Loader size="xl" /> 381 </View> 382 ) : labelersError || !labelers ? ( 383 <View style={[a.p_lg, a.rounded_sm, t.atoms.bg_contrast_25]}> 384 <Text> 385 <Trans> 386 We were unable to load your configured labelers at this time. 387 </Trans> 388 </Text> 389 </View> 390 ) : ( 391 <View style={[a.rounded_sm, t.atoms.bg_contrast_25]}> 392 {labelers.map((labeler, i) => { 393 return ( 394 <React.Fragment key={labeler.creator.did}> 395 {i !== 0 && <Divider />} 396 <LabelingService.Link labeler={labeler}> 397 {state => ( 398 <LabelingService.Outer 399 style={[ 400 i === 0 && { 401 borderTopLeftRadius: a.rounded_sm.borderRadius, 402 borderTopRightRadius: a.rounded_sm.borderRadius, 403 }, 404 i === labelers.length - 1 && { 405 borderBottomLeftRadius: a.rounded_sm.borderRadius, 406 borderBottomRightRadius: a.rounded_sm.borderRadius, 407 }, 408 (state.hovered || state.pressed) && [ 409 t.atoms.bg_contrast_50, 410 ], 411 ]}> 412 <LabelingService.Avatar avatar={labeler.creator.avatar} /> 413 <LabelingService.Content> 414 <LabelingService.Title 415 value={getLabelingServiceTitle({ 416 displayName: labeler.creator.displayName, 417 handle: labeler.creator.handle, 418 })} 419 /> 420 <LabelingService.Description 421 value={labeler.creator.description} 422 handle={labeler.creator.handle} 423 /> 424 </LabelingService.Content> 425 </LabelingService.Outer> 426 )} 427 </LabelingService.Link> 428 </React.Fragment> 429 ) 430 })} 431 </View> 432 )} 433 434 <Text 435 style={[ 436 a.text_md, 437 a.font_bold, 438 a.pt_2xl, 439 a.pb_md, 440 t.atoms.text_contrast_high, 441 ]}> 442 <Trans>Logged-out visibility</Trans> 443 </Text> 444 445 <PwiOptOut /> 446 447 <View style={{height: 200}} /> 448 </ScrollView> 449 ) 450} 451 452function PwiOptOut() { 453 const t = useTheme() 454 const {_} = useLingui() 455 const {currentAccount} = useSession() 456 const {data: profile} = useProfileQuery({did: currentAccount?.did}) 457 const updateProfile = useProfileUpdateMutation() 458 459 const isOptedOut = 460 profile?.labels?.some(l => l.val === '!no-unauthenticated') || false 461 const canToggle = profile && !updateProfile.isPending 462 463 const onToggleOptOut = React.useCallback(() => { 464 if (!profile) { 465 return 466 } 467 let wasAdded = false 468 updateProfile.mutate({ 469 profile, 470 updates: existing => { 471 // create labels attr if needed 472 existing.labels = ComAtprotoLabelDefs.isSelfLabels(existing.labels) 473 ? existing.labels 474 : { 475 $type: 'com.atproto.label.defs#selfLabels', 476 values: [], 477 } 478 479 // toggle the label 480 const hasLabel = existing.labels.values.some( 481 l => l.val === '!no-unauthenticated', 482 ) 483 if (hasLabel) { 484 wasAdded = false 485 existing.labels.values = existing.labels.values.filter( 486 l => l.val !== '!no-unauthenticated', 487 ) 488 } else { 489 wasAdded = true 490 existing.labels.values.push({val: '!no-unauthenticated'}) 491 } 492 493 // delete if no longer needed 494 if (existing.labels.values.length === 0) { 495 delete existing.labels 496 } 497 return existing 498 }, 499 checkCommitted: res => { 500 const exists = !!res.data.labels?.some( 501 l => l.val === '!no-unauthenticated', 502 ) 503 return exists === wasAdded 504 }, 505 }) 506 }, [updateProfile, profile]) 507 508 return ( 509 <View style={[a.pt_sm]}> 510 <View style={[a.flex_row, a.align_center, a.justify_between, a.gap_lg]}> 511 <Toggle.Item 512 disabled={!canToggle} 513 value={isOptedOut} 514 onChange={onToggleOptOut} 515 name="logged_out_visibility" 516 style={a.flex_1} 517 label={_( 518 msg`Discourage apps from showing my account to logged-out users`, 519 )}> 520 <Toggle.Switch /> 521 <Toggle.Label style={[a.text_md, a.flex_1]}> 522 <Trans> 523 Discourage apps from showing my account to logged-out users 524 </Trans> 525 </Toggle.Label> 526 </Toggle.Item> 527 528 {updateProfile.isPending && <Loader />} 529 </View> 530 531 <View style={[a.pt_md, a.gap_md, {paddingLeft: 38}]}> 532 <Text style={[a.leading_snug, t.atoms.text_contrast_high]}> 533 <Trans> 534 Bluesky will not show your profile and posts to logged-out users. 535 Other apps may not honor this request. This does not make your 536 account private. 537 </Trans> 538 </Text> 539 <Text style={[a.font_bold, a.leading_snug, t.atoms.text_contrast_high]}> 540 <Trans> 541 Note: Bluesky is an open and public network. This setting only 542 limits the visibility of your content on the Bluesky app and 543 website, and other apps may not respect this setting. Your content 544 may still be shown to logged-out users by other apps and websites. 545 </Trans> 546 </Text> 547 548 <InlineLink to="https://blueskyweb.zendesk.com/hc/en-us/articles/15835264007693-Data-Privacy"> 549 <Trans>Learn more about what is public on Bluesky.</Trans> 550 </InlineLink> 551 </View> 552 </View> 553 ) 554}