import React from 'react' import {View} from 'react-native' import {useFocusEffect} from '@react-navigation/native' import {ComAtprotoLabelDefs} from '@atproto/api' import {Trans, msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {LABELS} from '@atproto/api' import {useSafeAreaFrame} from 'react-native-safe-area-context' import {NativeStackScreenProps, CommonNavigatorParams} from '#/lib/routes/types' import {CenteredView} from '#/view/com/util/Views' import {ViewHeader} from '#/view/com/util/ViewHeader' import {useAnalytics} from 'lib/analytics/analytics' import {useSetMinimalShellMode} from '#/state/shell' import {useSession} from '#/state/session' import { useProfileQuery, useProfileUpdateMutation, } from '#/state/queries/profile' import {ScrollView} from '#/view/com/util/Views' import { UsePreferencesQueryResponse, useMyLabelersQuery, usePreferencesQuery, usePreferencesSetAdultContentMutation, } from '#/state/queries/preferences' import {getLabelingServiceTitle} from '#/lib/moderation' import {logger} from '#/logger' import {useTheme, atoms as a, useBreakpoints, ViewStyleProp} from '#/alf' import {Divider} from '#/components/Divider' import {CircleBanSign_Stroke2_Corner0_Rounded as CircleBanSign} from '#/components/icons/CircleBanSign' import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group' import {Person_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person' import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron' import {Filter_Stroke2_Corner0_Rounded as Filter} from '#/components/icons/Filter' import {Text} from '#/components/Typography' import * as Toggle from '#/components/forms/Toggle' import {InlineLink, Link} from '#/components/Link' import {Button, ButtonText} from '#/components/Button' import {Loader} from '#/components/Loader' import * as LabelingService from '#/components/LabelingServiceCard' import {GlobalModerationLabelPref} from '#/components/moderation/GlobalModerationLabelPref' import {useGlobalDialogsControlContext} from '#/components/dialogs/Context' import {Props as SVGIconProps} from '#/components/icons/common' import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings' import * as Dialog from '#/components/Dialog' function ErrorState({error}: {error: string}) { const t = useTheme() return ( Hmmmm, it seems we're having trouble loading this data. See below for more details. If this issue persists, please contact us. {error} ) } export function ModerationScreen( _props: NativeStackScreenProps, ) { const t = useTheme() const {_} = useLingui() const { isLoading: isPreferencesLoading, error: preferencesError, data: preferences, } = usePreferencesQuery() const {gtMobile} = useBreakpoints() const {height} = useSafeAreaFrame() const isLoading = isPreferencesLoading const error = preferencesError return ( {isLoading ? ( ) : error || !preferences ? ( ) : ( )} ) } function SubItem({ title, icon: Icon, style, }: ViewStyleProp & { title: string icon: React.ComponentType }) { const t = useTheme() return ( {title} ) } export function ModerationScreenInner({ preferences, }: { preferences: UsePreferencesQueryResponse }) { const {_} = useLingui() const t = useTheme() const setMinimalShellMode = useSetMinimalShellMode() const {screen} = useAnalytics() const {gtMobile} = useBreakpoints() const {mutedWordsDialogControl} = useGlobalDialogsControlContext() const birthdateDialogControl = Dialog.useDialogControl() const { isLoading: isLabelersLoading, data: labelers, error: labelersError, } = useMyLabelersQuery() useFocusEffect( React.useCallback(() => { screen('Moderation') setMinimalShellMode(false) }, [screen, setMinimalShellMode]), ) const {mutateAsync: setAdultContentPref, variables: optimisticAdultContent} = usePreferencesSetAdultContentMutation() const adultContentEnabled = !!( (optimisticAdultContent && optimisticAdultContent.enabled) || (!optimisticAdultContent && preferences.moderationPrefs.adultContentEnabled) ) const ageNotSet = !preferences.userAge const isUnderage = (preferences.userAge || 0) < 18 const onToggleAdultContentEnabled = React.useCallback( async (selected: boolean) => { try { await setAdultContentPref({ enabled: selected, }) } catch (e: any) { logger.error(`Failed to set adult content pref`, { message: e.message, }) } }, [setAdultContentPref], ) return ( Moderation tools {state => ( )} {state => ( )} {state => ( )} Content filters {ageNotSet && ( <> )} {!ageNotSet && !isUnderage && ( <> Enable adult content {adultContentEnabled ? ( Enabled ) : ( Disabled )} )} {!isUnderage && adultContentEnabled && ( <> )} Advanced {isLabelersLoading ? ( ) : labelersError || !labelers ? ( We were unable to load your configured labelers at this time. ) : ( {labelers.map((labeler, i) => { return ( {i !== 0 && } {state => ( )} ) })} )} Logged-out visibility ) } function PwiOptOut() { const t = useTheme() const {_} = useLingui() const {currentAccount} = useSession() const {data: profile} = useProfileQuery({did: currentAccount?.did}) const updateProfile = useProfileUpdateMutation() const isOptedOut = profile?.labels?.some(l => l.val === '!no-unauthenticated') || false const canToggle = profile && !updateProfile.isPending const onToggleOptOut = React.useCallback(() => { if (!profile) { return } let wasAdded = false updateProfile.mutate({ profile, updates: existing => { // create labels attr if needed existing.labels = ComAtprotoLabelDefs.isSelfLabels(existing.labels) ? existing.labels : { $type: 'com.atproto.label.defs#selfLabels', values: [], } // toggle the label const hasLabel = existing.labels.values.some( l => l.val === '!no-unauthenticated', ) if (hasLabel) { wasAdded = false existing.labels.values = existing.labels.values.filter( l => l.val !== '!no-unauthenticated', ) } else { wasAdded = true existing.labels.values.push({val: '!no-unauthenticated'}) } // delete if no longer needed if (existing.labels.values.length === 0) { delete existing.labels } return existing }, checkCommitted: res => { const exists = !!res.data.labels?.some( l => l.val === '!no-unauthenticated', ) return exists === wasAdded }, }) }, [updateProfile, profile]) return ( Discourage apps from showing my account to logged-out users {updateProfile.isPending && } Bluesky will not show your profile and posts to logged-out users. Other apps may not honor this request. This does not make your account private. Note: Bluesky is an open and public network. This setting only limits the visibility of your content on the Bluesky app and website, and other apps may not respect this setting. Your content may still be shown to logged-out users by other apps and websites. Learn more about what is public on Bluesky. ) }