import {useState} from 'react' import {Alert, LayoutAnimation, Linking, Pressable, View} from 'react-native' import {useReducedMotion} from 'react-native-reanimated' import {removeNuxs} from '@bsky.app/sdk' import {moderateProfile} from '@bsky.app/sdk/moderation' import {Trans, useLingui} from '@lingui/react/macro' import {useNavigation} from '@react-navigation/native' import {type NativeStackScreenProps} from '@react-navigation/native-stack' import {HELP_DESK_URL, HITSLOP_10} from '#/lib/constants' import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher' import {useApplyPullRequestOTAUpdate} from '#/lib/hooks/useOTAUpdates' import { type CommonNavigatorParams, type NavigationProp, } from '#/lib/routes/types' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' import {useProfileShadow} from '#/state/cache/profile-shadow' import * as persisted from '#/state/persisted' import {clearStorage} from '#/state/persisted' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useDeleteActorDeclaration} from '#/state/queries/messages/actor-declaration' import {useProfileQuery, useProfilesQuery} from '#/state/queries/profile' import { type SessionAccount, usePdsClient, useSession, useSessionApi, } from '#/state/session' import {useOnboardingDispatch} from '#/state/shell' import {useLoggedOutViewControls} from '#/state/shell/logged-out' import {useCloseAllActiveElements} from '#/state/util' import {UserAvatar} from '#/view/com/util/UserAvatar' import {GrowthbookDialog} from '#/screens/Settings/components/GrowthbookDialog' import * as SettingsList from '#/screens/Settings/components/SettingsList' import {atoms as a, platform, tokens, useBreakpoints, useTheme} from '#/alf' import {AgeAssuranceDismissibleNotice} from '#/components/ageAssurance/AgeAssuranceDismissibleNotice' import {AvatarStackWithFetch} from '#/components/AvatarStack' import {Button, ButtonText} from '#/components/Button' import {useIsFindContactsFeatureEnabledBasedOnGeolocation} from '#/components/contacts/country-allowlist' import {useDialogControl} from '#/components/Dialog' import {SwitchAccountDialog} from '#/components/dialogs/SwitchAccount' import {Accessibility_Stroke2_Corner2_Rounded as AccessibilityIcon} from '#/components/icons/Accessibility' import {Beaker_Stroke2_Corner2_Rounded as BeakerIcon} from '#/components/icons/Beaker' import {Bell_Stroke2_Corner0_Rounded as NotificationIcon} from '#/components/icons/Bell' import {BubbleInfo_Stroke2_Corner2_Rounded as BubbleInfoIcon} from '#/components/icons/BubbleInfo' import {ChevronTop_Stroke2_Corner0_Rounded as ChevronUpIcon} from '#/components/icons/Chevron' import {CircleQuestion_Stroke2_Corner2_Rounded as CircleQuestionIcon} from '#/components/icons/CircleQuestion' import {CodeBrackets_Stroke2_Corner2_Rounded as CodeBracketsIcon} from '#/components/icons/CodeBrackets' import {Contacts_Stroke2_Corner2_Rounded as ContactsIcon} from '#/components/icons/Contacts' import {DotGrid3x1_Stroke2_Corner0_Rounded as DotsHorizontal} from '#/components/icons/DotGrid' import {Earth_Stroke2_Corner2_Rounded as EarthIcon} from '#/components/icons/Globe' import {Lock_Stroke2_Corner2_Rounded as LockIcon} from '#/components/icons/Lock' import {PaintRoller_Stroke2_Corner2_Rounded as PaintRollerIcon} from '#/components/icons/PaintRoller' import { Person_Stroke2_Corner2_Rounded as PersonIcon, PersonGroup_Stroke2_Corner2_Rounded as PersonGroupIcon, PersonPlus_Stroke2_Corner2_Rounded as PersonPlusIcon, PersonX_Stroke2_Corner0_Rounded as PersonXIcon, } from '#/components/icons/Person' import {RaisingHand4Finger_Stroke2_Corner2_Rounded as HandIcon} from '#/components/icons/RaisingHand' import {Window_Stroke2_Corner2_Rounded as WindowIcon} from '#/components/icons/Window' import * as Layout from '#/components/Layout' import {Loader} from '#/components/Loader' import * as Menu from '#/components/Menu' import {ID as PolicyUpdate202508} from '#/components/PolicyUpdateOverlay/updates/202508/config' import {ProfileBadges} from '#/components/ProfileBadges' import * as Prompt from '#/components/Prompt' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' import {IS_INTERNAL, IS_IOS, IS_NATIVE} from '#/env' import {useActorStatus} from '#/features/liveNow' import {type app} from '#/lexicons' import {device, useStorage} from '#/storage' import {useActivitySubscriptionsNudged} from '#/storage/hooks/activity-subscriptions-nudged' type Props = NativeStackScreenProps export function SettingsScreen({}: Props) { const ax = useAnalytics() const {t: l} = useLingui() const reducedMotion = useReducedMotion() const {logoutEveryAccount} = useSessionApi() const {accounts, currentAccount} = useSession() const switchAccountControl = useDialogControl() const signOutPromptControl = Prompt.usePromptControl() const {data: profile} = useProfileQuery({did: currentAccount?.did}) const {data: otherProfiles} = useProfilesQuery({ handles: accounts .filter(acc => acc.did !== currentAccount?.did) .map(acc => acc.handle), }) const {pendingDid, onPressSwitchAccount} = useAccountSwitcher() const [showAccounts, setShowAccounts] = useState(false) const [showDevOptions, setShowDevOptions] = useState(false) const findContactsEnabled = useIsFindContactsFeatureEnabledBasedOnGeolocation() return ( Settings {profile && } {accounts.length > 1 ? ( <> { if (!reducedMotion) { LayoutAnimation.configureNext( LayoutAnimation.Presets.easeInEaseOut, ) } setShowAccounts(s => !s) }}> Switch account {showAccounts ? ( ) : ( acc.did) .filter(did => did !== currentAccount?.did) .slice(0, 5)} /> )} {showAccounts && ( <> {accounts .filter(acc => acc.did !== currentAccount?.did) .map(account => ( p.did === account.did, )} pendingDid={pendingDid} onPressSwitchAccount={(account, logContext) => void onPressSwitchAccount(account, logContext) } /> ))} )} ) : ( )} Account Privacy and security Moderation and content filters Notifications Content and media {IS_NATIVE && findContactsEnabled && !ax.features.enabled(ax.features.ImportContactsSettingsDisable) && ( Find and invite friends )} Appearance Accessibility Languages Beta features void Linking.openURL(HELP_DESK_URL)} label={l`Help`} accessibilityHint={l`Opens helpdesk in browser`}> Help About signOutPromptControl.open()} label={l`Sign out`}> Sign out {IS_INTERNAL && ( <> { if (!reducedMotion) { LayoutAnimation.configureNext( LayoutAnimation.Presets.easeInEaseOut, ) } setShowDevOptions(d => !d) }} label={l`Developer options`}> Developer options {showDevOptions && } )} logoutEveryAccount('Settings')} confirmButtonCta={l`Sign out`} cancelButtonCta={l`Cancel`} confirmButtonColor="negative" /> ) } function ProfilePreview({ profile, }: { profile: app.bsky.actor.defs.ProfileViewDetailed }) { const t = useTheme() const {gtMobile} = useBreakpoints() const shadow = useProfileShadow(profile) const moderationOpts = useModerationOpts() const {isActive: live} = useActorStatus(profile) if (!moderationOpts) return null const moderation = moderateProfile(profile, moderationOpts) const displayName = sanitizeDisplayName( profile.displayName || sanitizeHandle(profile.handle), moderation.ui('displayName'), ) return ( <> {displayName} {sanitizeHandle(profile.handle, '@')} ) } function DevOptions() { const {t: l} = useLingui() const pdsClient = usePdsClient() const [override, setOverride] = useStorage(device, [ 'policyUpdateDebugOverride', ]) const onboardingDispatch = useOnboardingDispatch() const navigation = useNavigation() const {mutate: deleteChatDeclarationRecord} = useDeleteActorDeclaration() const { tryApplyUpdate, restoreDefaultChannel, isCurrentlyRunningPullRequestDeployment, currentChannel, } = useApplyPullRequestOTAUpdate() const [actyNotifNudged, setActyNotifNudged] = useActivitySubscriptionsNudged() const growthbookControl = useDialogControl() const resetOnboarding = () => { navigation.navigate('Home') onboardingDispatch({type: 'start'}) Toast.show(l`Onboarding reset`) } const clearAllStorage = async () => { await clearStorage() Toast.show(l`Storage cleared, you need to restart the app now.`) } const onPressUnsnoozeReminder = () => { const lastEmailConfirm = new Date() // wind back 3 days lastEmailConfirm.setDate(lastEmailConfirm.getDate() - 3) void persisted.write('reminders', { ...persisted.get('reminders'), lastEmailConfirm: lastEmailConfirm.toISOString(), }) Toast.show(l`You probably want to restart the app now.`) } const onPressActySubsUnNudge = () => { setActyNotifNudged(false) } const onPressApplyOta = () => { Alert.prompt( 'Apply OTA', 'Enter the channel for the OTA you wish to apply.', [ { style: 'cancel', text: 'Cancel', }, { style: 'default', text: 'Apply', onPress: (channel?: string) => { void tryApplyUpdate(channel ?? '') }, }, ], 'plain-text', isCurrentlyRunningPullRequestDeployment ? currentChannel : 'pull-request-', ) } return ( <> navigation.navigate('Log')} label={l`Open system log`}> System log growthbookControl.open()} label={l`View GrowthBook information`}> GrowthBook navigation.navigate('Debug')} label={l`Open storybook page`}> Storybook navigation.navigate('DebugMod')} label={l`Open moderation debug page`}> Debug Moderation deleteChatDeclarationRecord()} label={l`Delete chat declaration record`}> Delete chat declaration record void resetOnboarding()} label={l`Reset onboarding state`}> Reset onboarding state Unsnooze email reminder {actyNotifNudged && ( Reset activity subscription nudge )} void clearAllStorage()} label={l`Clear all storage data`}> Clear all storage data (restart after this) {IS_IOS ? ( Apply Pull Request ) : null} {IS_NATIVE && isCurrentlyRunningPullRequestDeployment ? ( void restoreDefaultChannel()} label={l`Unapply Pull Request`}> Unapply Pull Request {currentChannel} ) : null} PolicyUpdate202508 Debug ) } function AddAccountRow() { const {t: l} = useLingui() const {setShowLoggedOut} = useLoggedOutViewControls() const closeEverything = useCloseAllActiveElements() const onAddAnotherAccount = () => { setShowLoggedOut(true) closeEverything() } return ( Add another account ) } function AccountRow({ profile, account, pendingDid, onPressSwitchAccount, }: { profile?: app.bsky.actor.defs.ProfileViewDetailed account: SessionAccount pendingDid: string | null onPressSwitchAccount: ( account: SessionAccount, logContext: 'Settings', ) => void }) { const {t: l} = useLingui() const t = useTheme() const moderationOpts = useModerationOpts() const removePromptControl = Prompt.usePromptControl() const {removeAccount} = useSessionApi() const {isActive: live} = useActorStatus(profile) const onSwitchAccount = () => { if (pendingDid) return onPressSwitchAccount(account, 'Settings') } return ( {moderationOpts && profile ? ( ) : ( )} {sanitizeHandle(account.handle, '@')} {pendingDid === account.did && } {!pendingDid && ( {({props, state}) => ( )} removePromptControl.open()}> Remove account )} { removeAccount(account) Toast.show(l`Account removed from quick access`) }} confirmButtonCta={l`Remove`} confirmButtonColor="negative" /> ) }