import React from 'react'
import {Linking, View} from 'react-native'
import {useSafeAreaFrame} from 'react-native-safe-area-context'
import {ComAtprotoLabelDefs} from '@atproto/api'
import {LABELS} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native'
import {getLabelingServiceTitle} from '#/lib/moderation'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {logger} from '#/logger'
import {isIOS} from '#/platform/detection'
import {
useMyLabelersQuery,
usePreferencesQuery,
UsePreferencesQueryResponse,
usePreferencesSetAdultContentMutation,
} from '#/state/queries/preferences'
import {
useProfileQuery,
useProfileUpdateMutation,
} from '#/state/queries/profile'
import {useSession} from '#/state/session'
import {isNonConfigurableModerationAuthority} from '#/state/session/additional-moderation-authorities'
import {useSetMinimalShellMode} from '#/state/shell'
import {ViewHeader} from '#/view/com/util/ViewHeader'
import {CenteredView} from '#/view/com/util/Views'
import {ScrollView} from '#/view/com/util/Views'
import {atoms as a, useBreakpoints, useTheme, ViewStyleProp} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings'
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
import {Divider} from '#/components/Divider'
import * as Toggle from '#/components/forms/Toggle'
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
import {CircleBanSign_Stroke2_Corner0_Rounded as CircleBanSign} from '#/components/icons/CircleBanSign'
import {Props as SVGIconProps} from '#/components/icons/common'
import {Filter_Stroke2_Corner0_Rounded as Filter} from '#/components/icons/Filter'
import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group'
import {Person_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person'
import * as LabelingService from '#/components/LabelingServiceCard'
import {InlineLinkText, Link} from '#/components/Link'
import {Loader} from '#/components/Loader'
import {GlobalLabelPreference} from '#/components/moderation/LabelPreference'
import {Text} from '#/components/Typography'
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 {gtMobile} = useBreakpoints()
const {mutedWordsDialogControl} = useGlobalDialogsControlContext()
const birthdateDialogControl = Dialog.useDialogControl()
const {
isLoading: isLabelersLoading,
data: labelers,
error: labelersError,
} = useMyLabelersQuery()
useFocusEffect(
React.useCallback(() => {
setMinimalShellMode(false)
}, [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],
)
const disabledOnIOS = isIOS && !adultContentEnabled
return (
Moderation tools
{state => (
)}
{state => (
)}
{state => (
)}
Content filters
{ageNotSet && (
<>
>
)}
{!ageNotSet && !isUnderage && (
<>
Enable adult content
{adultContentEnabled ? (
Enabled
) : (
Disabled
)}
{disabledOnIOS && (
Adult content can only be enabled via the Web at{' '}
{
evt.preventDefault()
Linking.openURL('https://bsky.app/')
return false
}}>
bsky.app
.
)}
>
)}
{!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 => (
{isNonConfigurableModerationAuthority(
labeler.creator.did,
) && }
)}
)
})}
)}
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.
)
}