diff --git a/assets/icons/bubbles_stroke2_corner2_rounded.svg b/assets/icons/bubbles_stroke2_corner2_rounded.svg
new file mode 100644
index 0000000000..f09fcaa33c
--- /dev/null
+++ b/assets/icons/bubbles_stroke2_corner2_rounded.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/car_stroke2_corner2_rounded.svg b/assets/icons/car_stroke2_corner2_rounded.svg
new file mode 100644
index 0000000000..ca29cefd1c
--- /dev/null
+++ b/assets/icons/car_stroke2_corner2_rounded.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/circleQuestion_stroke2_corner2_rounded.svg b/assets/icons/circleQuestion_stroke2_corner2_rounded.svg
index a534f98716..c025a14509 100644
--- a/assets/icons/circleQuestion_stroke2_corner2_rounded.svg
+++ b/assets/icons/circleQuestion_stroke2_corner2_rounded.svg
@@ -1 +1 @@
-
+
diff --git a/assets/icons/codeBrackets_stroke2_corner2_rounded.svg b/assets/icons/codeBrackets_stroke2_corner2_rounded.svg
new file mode 100644
index 0000000000..d40e7b003d
--- /dev/null
+++ b/assets/icons/codeBrackets_stroke2_corner2_rounded.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/codeLines_stroke2_corner2_rounded.svg b/assets/icons/codeLines_stroke2_corner2_rounded.svg
new file mode 100644
index 0000000000..65a7b1e856
--- /dev/null
+++ b/assets/icons/codeLines_stroke2_corner2_rounded.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/earth_stroke2_corner2_rounded.svg b/assets/icons/earth_stroke2_corner2_rounded.svg
new file mode 100644
index 0000000000..b8922629a3
--- /dev/null
+++ b/assets/icons/earth_stroke2_corner2_rounded.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/freeze_stroke2_corner2_rounded.svg b/assets/icons/freeze_stroke2_corner2_rounded.svg
new file mode 100644
index 0000000000..1091454782
--- /dev/null
+++ b/assets/icons/freeze_stroke2_corner2_rounded.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/haptic_stroke2_corner2_rounded.svg b/assets/icons/haptic_stroke2_corner2_rounded.svg
new file mode 100644
index 0000000000..7a93daf669
--- /dev/null
+++ b/assets/icons/haptic_stroke2_corner2_rounded.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/home_stroke2_corner2_rounded.svg b/assets/icons/home_stroke2_corner2_rounded.svg
new file mode 100644
index 0000000000..5665b24b88
--- /dev/null
+++ b/assets/icons/home_stroke2_corner2_rounded.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/key_stroke2_corner2_rounded.svg b/assets/icons/key_stroke2_corner2_rounded.svg
new file mode 100644
index 0000000000..2beb2b4ae4
--- /dev/null
+++ b/assets/icons/key_stroke2_corner2_rounded.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/macintosh_stroke2_corner2_rounded.svg b/assets/icons/macintosh_stroke2_corner2_rounded.svg
new file mode 100644
index 0000000000..5435319722
--- /dev/null
+++ b/assets/icons/macintosh_stroke2_corner2_rounded.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/newspaper_stroke2_corner2_rounded.svg b/assets/icons/newspaper_stroke2_corner2_rounded.svg
new file mode 100644
index 0000000000..3eb823cf2c
--- /dev/null
+++ b/assets/icons/newspaper_stroke2_corner2_rounded.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/pencilLine_stroke2_corner2_rounded.svg b/assets/icons/pencilLine_stroke2_corner2_rounded.svg
new file mode 100644
index 0000000000..92f1fed1c4
--- /dev/null
+++ b/assets/icons/pencilLine_stroke2_corner2_rounded.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/personGroup_stroke2_corner2_rounded.svg b/assets/icons/personGroup_stroke2_corner2_rounded.svg
new file mode 100644
index 0000000000..fad8a8e7fb
--- /dev/null
+++ b/assets/icons/personGroup_stroke2_corner2_rounded.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/raisingHand4finger_stroke2_corner2_rounded.svg b/assets/icons/raisingHand4finger_stroke2_corner2_rounded.svg
new file mode 100644
index 0000000000..f6beb5647d
--- /dev/null
+++ b/assets/icons/raisingHand4finger_stroke2_corner2_rounded.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/wrench_stroke2_corner2_rounded.svg b/assets/icons/wrench_stroke2_corner2_rounded.svg
new file mode 100644
index 0000000000..601614c88f
--- /dev/null
+++ b/assets/icons/wrench_stroke2_corner2_rounded.svg
@@ -0,0 +1 @@
+
diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go
index 12b7b8960f..d0d6b4c494 100644
--- a/bskyweb/cmd/bskyweb/server.go
+++ b/bskyweb/cmd/bskyweb/server.go
@@ -253,6 +253,10 @@ func serve(cctx *cli.Context) error {
e.GET("/settings/external-embeds", server.WebGeneric)
e.GET("/settings/accessibility", server.WebGeneric)
e.GET("/settings/appearance", server.WebGeneric)
+ e.GET("/settings/account", server.WebGeneric)
+ e.GET("/settings/privacy-and-security", server.WebGeneric)
+ e.GET("/settings/content-and-media", server.WebGeneric)
+ e.GET("/settings/about", server.WebGeneric)
e.GET("/sys/debug", server.WebGeneric)
e.GET("/sys/debug-mod", server.WebGeneric)
e.GET("/sys/log", server.WebGeneric)
diff --git a/src/Navigation.tsx b/src/Navigation.tsx
index 81d08c7da3..436f2488a5 100644
--- a/src/Navigation.tsx
+++ b/src/Navigation.tsx
@@ -95,6 +95,10 @@ import {Wizard} from '#/screens/StarterPack/Wizard'
import {useTheme} from '#/alf'
import {router} from '#/routes'
import {Referrer} from '../modules/expo-bluesky-swiss-army'
+import {AboutSettingsScreen} from './screens/Settings/AboutSettings'
+import {AccountSettingsScreen} from './screens/Settings/AccountSettings'
+import {ContentAndMediaSettingsScreen} from './screens/Settings/ContentAndMediaSettings'
+import {PrivacyAndSecuritySettingsScreen} from './screens/Settings/PrivacyAndSecuritySettings'
const navigationRef = createNavigationContainerRef()
@@ -322,7 +326,39 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
name="AppearanceSettings"
getComponent={() => AppearanceSettingsScreen}
options={{
- title: title(msg`Appearance Settings`),
+ title: title(msg`Appearance`),
+ requireAuth: true,
+ }}
+ />
+ AccountSettingsScreen}
+ options={{
+ title: title(msg`Account`),
+ requireAuth: true,
+ }}
+ />
+ PrivacyAndSecuritySettingsScreen}
+ options={{
+ title: title(msg`Privacy and Security`),
+ requireAuth: true,
+ }}
+ />
+ ContentAndMediaSettingsScreen}
+ options={{
+ title: title(msg`Content and Media`),
+ requireAuth: true,
+ }}
+ />
+ AboutSettingsScreen}
+ options={{
+ title: title(msg`About`),
requireAuth: true,
}}
/>
diff --git a/src/alf/atoms.ts b/src/alf/atoms.ts
index ca4c14dc6d..434420a972 100644
--- a/src/alf/atoms.ts
+++ b/src/alf/atoms.ts
@@ -102,6 +102,9 @@ export const atoms = {
/*
* Flex
*/
+ gap_0: {
+ gap: 0,
+ },
gap_2xs: {
gap: tokens.space._2xs,
},
diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx
index 57e3731645..ea11e22174 100644
--- a/src/components/Layout.tsx
+++ b/src/components/Layout.tsx
@@ -1,16 +1,22 @@
-import React from 'react'
+import React, {useContext, useMemo} from 'react'
import {View, ViewStyle} from 'react-native'
import {StyleProp} from 'react-native'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
+import {ViewHeader} from '#/view/com/util/ViewHeader'
+import {ScrollView} from '#/view/com/util/Views'
+import {CenteredView} from '#/view/com/util/Views'
import {atoms as a} from '#/alf'
// Every screen should have a Layout component wrapping it.
// This component provides a default padding for the top of the screen.
// This allows certain screens to avoid the top padding if they want to.
-//
-// In a future PR I will add a unified header component to this file and
-// things like a preconfigured scrollview.
+
+const LayoutContext = React.createContext({
+ withinScreen: false,
+ topPaddingDisabled: false,
+ withinScrollView: false,
+})
/**
* Every screen should have a Layout.Screen component wrapping it.
@@ -18,7 +24,7 @@ import {atoms as a} from '#/alf'
* and height/minHeight
*/
let Screen = ({
- disableTopPadding,
+ disableTopPadding = false,
style,
...props
}: React.ComponentProps & {
@@ -26,16 +32,69 @@ let Screen = ({
style?: StyleProp
}): React.ReactNode => {
const {top} = useSafeAreaInsets()
+ const context = useMemo(
+ () => ({
+ withinScreen: true,
+ topPaddingDisabled: disableTopPadding,
+ withinScrollView: false,
+ }),
+ [disableTopPadding],
+ )
return (
-
+
+
+
)
}
Screen = React.memo(Screen)
export {Screen}
+
+let Header = (
+ props: React.ComponentProps,
+): React.ReactNode => {
+ const {withinScrollView} = useContext(LayoutContext)
+ if (!withinScrollView) {
+ return (
+
+
+
+ )
+ } else {
+ return
+ }
+}
+Header = React.memo(Header)
+export {Header}
+
+let Content = ({
+ style,
+ contentContainerStyle,
+ ...props
+}: React.ComponentProps & {
+ style?: StyleProp
+ contentContainerStyle?: StyleProp
+}): React.ReactNode => {
+ const context = useContext(LayoutContext)
+ const newContext = useMemo(
+ () => ({...context, withinScrollView: true}),
+ [context],
+ )
+ return (
+
+
+
+ )
+}
+Content = React.memo(Content)
+export {Content}
diff --git a/src/components/dialogs/BirthDateSettings.tsx b/src/components/dialogs/BirthDateSettings.tsx
index 81d0c6740e..8f47d05b06 100644
--- a/src/components/dialogs/BirthDateSettings.tsx
+++ b/src/components/dialogs/BirthDateSettings.tsx
@@ -29,7 +29,7 @@ export function BirthDateSettingsDialog({
const {isLoading, error, data: preferences} = usePreferencesQuery()
return (
-
+
diff --git a/src/components/dialogs/SwitchAccount.tsx b/src/components/dialogs/SwitchAccount.tsx
index ea870e2da9..daad01d2ab 100644
--- a/src/components/dialogs/SwitchAccount.tsx
+++ b/src/components/dialogs/SwitchAccount.tsx
@@ -56,6 +56,8 @@ export function SwitchAccountDialog({
pendingDid={pendingDid}
/>
+
+
)
diff --git a/src/components/icons/Bubble.tsx b/src/components/icons/Bubble.tsx
index ff6da25316..3654fcf785 100644
--- a/src/components/icons/Bubble.tsx
+++ b/src/components/icons/Bubble.tsx
@@ -11,3 +11,7 @@ export const Bubble_Stroke2_Corner2_Rounded = createSinglePathSVG({
export const Bubble_Stroke2_Corner3_Rounded = createSinglePathSVG({
path: 'M2.002 7a4 4 0 0 1 4-4h12a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H12.28l-4.762 2.858A1 1 0 0 1 6.002 21v-2a4 4 0 0 1-4-4V7Zm4-2a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h1a1 1 0 0 1 1 1v1.234l3.486-2.092a1 1 0 0 1 .514-.142h6a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-12Z',
})
+
+export const Bubbles_Stroke2_Corner2_Rounded = createSinglePathSVG({
+ path: 'M6.002 6a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3h-1v1a3 3 0 0 1-3 3h-4.24l-4.274 2.374a1 1 0 0 1-1.486-.874V19a3 3 0 0 1-3-3v-6a3 3 0 0 1 3-3h1V6Zm-1 3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h1a1 1 0 0 1 1 1v.8l3.015-1.674a1 1 0 0 1 .485-.126h4.5a1 1 0 0 0 1-1v-1.933a1 1 0 0 1 0-.134V10a1 1 0 0 0-1-1h-10Zm13 4v-3a3 3 0 0 0-3-3h-7V6a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1h-1Z',
+})
diff --git a/src/components/icons/Car.tsx b/src/components/icons/Car.tsx
new file mode 100644
index 0000000000..25859176ac
--- /dev/null
+++ b/src/components/icons/Car.tsx
@@ -0,0 +1,5 @@
+import {createSinglePathSVG} from './TEMPLATE'
+
+export const Car_Stroke2_Corner2_Rounded = createSinglePathSVG({
+ path: 'M7.018 6a1 1 0 0 0-.808.412L5.4 5.824l.809.588L3 10.825V17a1 1 0 1 0 2 0 1 1 0 0 1 1-1h12a1 1 0 0 1 1 1 1 1 0 1 0 2 0v-5.998l-3.22-4.577A1 1 0 0 0 16.962 6H7.018ZM23 11.686V17a3 3 0 0 1-5.83 1H6.83A3.001 3.001 0 0 1 1 17v-5.5a1 1 0 1 1 0-2h.49l3.102-4.265A3 3 0 0 1 7.018 4h9.944a3 3 0 0 1 2.453 1.274l3.104 4.412H23a1 1 0 1 1 0 2ZM5 13a1 1 0 0 1 1-1h2a1 1 0 1 1 0 2H6a1 1 0 0 1-1-1Zm10 0a1 1 0 0 1 1-1h2a1 1 0 1 1 0 2h-2a1 1 0 0 1-1-1Z',
+})
diff --git a/src/components/icons/CircleQuestion.tsx b/src/components/icons/CircleQuestion.tsx
index 4eb369379b..6242a2b514 100644
--- a/src/components/icons/CircleQuestion.tsx
+++ b/src/components/icons/CircleQuestion.tsx
@@ -1,5 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'
export const CircleQuestion_Stroke2_Corner2_Rounded = createSinglePathSVG({
- path: 'M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Z" clip-rule="evenodd"/>
+
+
+ Terms of Service
+
+
+
+
+
+ Privacy Policy
+
+
+
+
+
+ Status Page
+
+
+
+
+
+
+ System log
+
+
+ {
+ setStringAsync(
+ `Build version: ${appVersion}; Bundle info: ${bundleInfo}; Bundle date: ${BUNDLE_DATE}; Platform: ${Platform.OS}; Platform version: ${Platform.Version}`,
+ )
+ Toast.show(_(msg`Copied build version to clipboard`))
+ }}>
+
+
+ Version {appVersion}
+
+ {bundleInfo}
+
+
+
+
+ )
+}
diff --git a/src/screens/Settings/AccessibilitySettings.tsx b/src/screens/Settings/AccessibilitySettings.tsx
new file mode 100644
index 0000000000..dfe2c14a5f
--- /dev/null
+++ b/src/screens/Settings/AccessibilitySettings.tsx
@@ -0,0 +1,113 @@
+import React from 'react'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {NativeStackScreenProps} from '@react-navigation/native-stack'
+
+import {CommonNavigatorParams} from '#/lib/routes/types'
+import {isNative} from '#/platform/detection'
+import {
+ useHapticsDisabled,
+ useRequireAltTextEnabled,
+ useSetHapticsDisabled,
+ useSetRequireAltTextEnabled,
+} from '#/state/preferences'
+import {
+ useLargeAltBadgeEnabled,
+ useSetLargeAltBadgeEnabled,
+} from '#/state/preferences/large-alt-badge'
+import * as SettingsList from '#/screens/Settings/components/SettingsList'
+import {atoms as a} from '#/alf'
+import {Admonition} from '#/components/Admonition'
+import * as Toggle from '#/components/forms/Toggle'
+import {Accessibility_Stroke2_Corner2_Rounded as AccessibilityIcon} from '#/components/icons/Accessibility'
+import {Haptic_Stroke2_Corner2_Rounded as HapticIcon} from '#/components/icons/Haptic'
+import * as Layout from '#/components/Layout'
+import {InlineLinkText} from '#/components/Link'
+
+type Props = NativeStackScreenProps<
+ CommonNavigatorParams,
+ 'AccessibilitySettings'
+>
+export function AccessibilitySettingsScreen({}: Props) {
+ const {_} = useLingui()
+
+ const requireAltTextEnabled = useRequireAltTextEnabled()
+ const setRequireAltTextEnabled = useSetRequireAltTextEnabled()
+ const hapticsDisabled = useHapticsDisabled()
+ const setHapticsDisabled = useSetHapticsDisabled()
+ const largeAltBadgeEnabled = useLargeAltBadgeEnabled()
+ const setLargeAltBadgeEnabled = useSetLargeAltBadgeEnabled()
+
+ return (
+
+
+
+
+
+
+
+ Alt text
+
+ setRequireAltTextEnabled(value)}
+ style={[a.w_full]}>
+
+ Require alt text before posting
+
+
+
+ setLargeAltBadgeEnabled(value)}
+ style={[a.w_full]}>
+
+ Display larger alt text badges
+
+
+
+
+ {isNative && (
+ <>
+
+
+
+
+ Haptics
+
+ setHapticsDisabled(value)}
+ style={[a.w_full]}>
+
+ Disable haptic feedback
+
+
+
+
+ >
+ )}
+
+
+
+ Autoplay options have moved to the{' '}
+
+ Content and Media settings
+
+ .
+
+
+
+
+
+
+ )
+}
diff --git a/src/screens/Settings/AccountSettings.tsx b/src/screens/Settings/AccountSettings.tsx
new file mode 100644
index 0000000000..19101d2f45
--- /dev/null
+++ b/src/screens/Settings/AccountSettings.tsx
@@ -0,0 +1,180 @@
+import React from 'react'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {NativeStackScreenProps} from '@react-navigation/native-stack'
+import {useQueryClient} from '@tanstack/react-query'
+
+import {CommonNavigatorParams} from '#/lib/routes/types'
+import {useModalControls} from '#/state/modals'
+import {RQKEY as RQKEY_PROFILE} from '#/state/queries/profile'
+import {useProfileQuery} from '#/state/queries/profile'
+import {useSession} from '#/state/session'
+import {ExportCarDialog} from '#/view/screens/Settings/ExportCarDialog'
+import * as SettingsList from '#/screens/Settings/components/SettingsList'
+import {atoms as a, useTheme} from '#/alf'
+import {useDialogControl} from '#/components/Dialog'
+import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings'
+import {At_Stroke2_Corner2_Rounded as AtIcon} from '#/components/icons/At'
+import {BirthdayCake_Stroke2_Corner2_Rounded as BirthdayCakeIcon} from '#/components/icons/BirthdayCake'
+import {Car_Stroke2_Corner2_Rounded as CarIcon} from '#/components/icons/Car'
+import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
+import {Envelope_Stroke2_Corner2_Rounded as EnvelopeIcon} from '#/components/icons/Envelope'
+import {Freeze_Stroke2_Corner2_Rounded as FreezeIcon} from '#/components/icons/Freeze'
+import {Lock_Stroke2_Corner2_Rounded as LockIcon} from '#/components/icons/Lock'
+import {PencilLine_Stroke2_Corner2_Rounded as PencilIcon} from '#/components/icons/Pencil'
+import {Trash_Stroke2_Corner2_Rounded} from '#/components/icons/Trash'
+import {Verified_Stroke2_Corner2_Rounded as VerifiedIcon} from '#/components/icons/Verified'
+import * as Layout from '#/components/Layout'
+import {DeactivateAccountDialog} from './components/DeactivateAccountDialog'
+
+type Props = NativeStackScreenProps
+export function AccountSettingsScreen({}: Props) {
+ const t = useTheme()
+ const {_} = useLingui()
+ const {currentAccount} = useSession()
+ const queryClient = useQueryClient()
+ const {data: profile} = useProfileQuery({did: currentAccount?.did})
+ const {openModal} = useModalControls()
+ const birthdayControl = useDialogControl()
+ const exportCarControl = useDialogControl()
+ const deactivateAccountControl = useDialogControl()
+
+ return (
+
+
+
+
+
+
+
+ Email
+
+ {currentAccount && (
+ <>
+
+ {currentAccount.email || (no email)}
+
+ {currentAccount.emailConfirmed ? (
+
+ ) : (
+ {}}
+ />
+ )}
+ >
+ )}
+
+ openModal({name: 'change-email'})}>
+
+
+ Change email
+
+
+
+
+
+
+ Protect your account
+
+
+
+
+
+
+ Birthday
+
+ birthdayControl.open()}
+ />
+
+ openModal({name: 'change-password'})}>
+
+
+ Password
+
+
+
+
+ openModal({
+ name: 'change-handle',
+ onChanged() {
+ if (currentAccount) {
+ // refresh my profile
+ queryClient.invalidateQueries({
+ queryKey: RQKEY_PROFILE(currentAccount.did),
+ })
+ }
+ },
+ })
+ }>
+
+
+ Handle
+
+ {profile && (
+ @{profile.handle}
+ )}
+
+
+
+ exportCarControl.open()}>
+
+
+ Export my data
+
+
+
+ deactivateAccountControl.open()}
+ destructive>
+
+
+ Deactivate account
+
+
+
+ openModal({name: 'delete-account'})}
+ destructive>
+
+
+ Delete account
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/screens/Settings/AppearanceSettings.tsx b/src/screens/Settings/AppearanceSettings.tsx
index c317c930fa..d0beb7d506 100644
--- a/src/screens/Settings/AppearanceSettings.tsx
+++ b/src/screens/Settings/AppearanceSettings.tsx
@@ -1,18 +1,15 @@
import React, {useCallback} from 'react'
-import {View} from 'react-native'
import Animated, {
- FadeInDown,
- FadeOutDown,
+ FadeInUp,
+ FadeOutUp,
LayoutAnimationConfig,
+ LinearTransition,
} from 'react-native-reanimated'
-import {msg, Trans} from '@lingui/macro'
+import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {useSetThemePrefs, useThemePrefs} from '#/state/shell'
-import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader'
-import {ScrollView} from '#/view/com/util/Views'
import {atoms as a, native, useAlf, useTheme} from '#/alf'
import * as ToggleButton from '#/components/forms/ToggleButton'
import {Props as SVGIconProps} from '#/components/icons/common'
@@ -22,12 +19,11 @@ import {TextSize_Stroke2_Corner0_Rounded as TextSize} from '#/components/icons/T
import {TitleCase_Stroke2_Corner0_Rounded as Aa} from '#/components/icons/TitleCase'
import * as Layout from '#/components/Layout'
import {Text} from '#/components/Typography'
+import * as SettingsList from './components/SettingsList'
type Props = NativeStackScreenProps
export function AppearanceSettingsScreen({}: Props) {
- const t = useTheme()
const {_} = useLingui()
- const {isTabletOrMobile} = useWebMediaQueries()
const {fonts} = useAlf()
const {colorMode, darkTheme} = useThemePrefs()
@@ -77,66 +73,54 @@ export function AppearanceSettingsScreen({}: Props) {
return (
-
-
-
-
- Appearance
-
-
-
+
+
+
+
-
-
-
-
- {colorMode !== 'light' && (
-
-
-
- )}
+ {colorMode !== 'light' && (
+
+
+
+ )}
+
-
-
-
+
+
+
)
@@ -205,29 +189,32 @@ export function AppearanceToggleButtonGroup({
}) {
const t = useTheme()
return (
-
-
-
-
- {title}
-
+ <>
+
+
+ {title}
{description && (
+ style={[
+ a.text_sm,
+ a.leading_snug,
+ t.atoms.text_contrast_medium,
+ a.w_full,
+ ]}>
{description}
)}
-
-
- {items.map(item => (
-
- {item.label}
-
- ))}
-
-
+
+ {items.map(item => (
+
+ {item.label}
+
+ ))}
+
+
+ >
)
}
diff --git a/src/screens/Settings/ContentAndMediaSettings.tsx b/src/screens/Settings/ContentAndMediaSettings.tsx
new file mode 100644
index 0000000000..79c8a48f3b
--- /dev/null
+++ b/src/screens/Settings/ContentAndMediaSettings.tsx
@@ -0,0 +1,104 @@
+import React from 'react'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {NativeStackScreenProps} from '@react-navigation/native-stack'
+
+import {CommonNavigatorParams} from '#/lib/routes/types'
+import {isNative} from '#/platform/detection'
+import {useAutoplayDisabled, useSetAutoplayDisabled} from '#/state/preferences'
+import {
+ useInAppBrowser,
+ useSetInAppBrowser,
+} from '#/state/preferences/in-app-browser'
+import * as SettingsList from '#/screens/Settings/components/SettingsList'
+import * as Toggle from '#/components/forms/Toggle'
+import {Bubbles_Stroke2_Corner2_Rounded as BubblesIcon} from '#/components/icons/Bubble'
+import {Hashtag_Stroke2_Corner0_Rounded as HashtagIcon} from '#/components/icons/Hashtag'
+import {Home_Stroke2_Corner2_Rounded as HomeIcon} from '#/components/icons/Home'
+import {Macintosh_Stroke2_Corner2_Rounded as MacintoshIcon} from '#/components/icons/Macintosh'
+import {Play_Stroke2_Corner2_Rounded as PlayIcon} from '#/components/icons/Play'
+import {Window_Stroke2_Corner2_Rounded as WindowIcon} from '#/components/icons/Window'
+import * as Layout from '#/components/Layout'
+
+type Props = NativeStackScreenProps<
+ CommonNavigatorParams,
+ 'ContentAndMediaSettings'
+>
+export function ContentAndMediaSettingsScreen({}: Props) {
+ const {_} = useLingui()
+ const autoplayDisabledPref = useAutoplayDisabled()
+ const setAutoplayDisabledPref = useSetAutoplayDisabled()
+ const inAppBrowserPref = useInAppBrowser()
+ const setUseInAppBrowser = useSetInAppBrowser()
+
+ return (
+
+
+
+
+
+
+
+ Manage saved feeds
+
+
+
+
+
+ Thread preferences
+
+
+
+
+
+ Following feed preferences
+
+
+
+
+
+ External media
+
+
+
+ {isNative && (
+ setUseInAppBrowser(value)}>
+
+
+
+ Use in-app browser to open links
+
+
+
+
+ )}
+ setAutoplayDisabledPref(value)}>
+
+
+
+ Disable autoplay for videos and GIFs
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/screens/Settings/PrivacyAndSecuritySettings.tsx b/src/screens/Settings/PrivacyAndSecuritySettings.tsx
new file mode 100644
index 0000000000..da462c90db
--- /dev/null
+++ b/src/screens/Settings/PrivacyAndSecuritySettings.tsx
@@ -0,0 +1,91 @@
+import React from 'react'
+import {View} from 'react-native'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {NativeStackScreenProps} from '@react-navigation/native-stack'
+
+import {CommonNavigatorParams} from '#/lib/routes/types'
+import {useAppPasswordsQuery} from '#/state/queries/app-passwords'
+import * as SettingsList from '#/screens/Settings/components/SettingsList'
+import {atoms as a} from '#/alf'
+import * as Admonition from '#/components/Admonition'
+import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlashIcon} from '#/components/icons/EyeSlash'
+import {Key_Stroke2_Corner2_Rounded as KeyIcon} from '#/components/icons/Key'
+import {Verified_Stroke2_Corner2_Rounded as VerifiedIcon} from '#/components/icons/Verified'
+import * as Layout from '#/components/Layout'
+import {InlineLinkText} from '#/components/Link'
+import {Email2FAToggle} from './components/Email2FAToggle'
+import {PwiOptOut} from './components/PwiOptOut'
+
+type Props = NativeStackScreenProps<
+ CommonNavigatorParams,
+ 'PrivacyAndSecuritySettings'
+>
+export function PrivacyAndSecuritySettingsScreen({}: Props) {
+ const {_} = useLingui()
+ const {data: appPasswords} = useAppPasswordsQuery()
+ return (
+
+
+
+
+
+
+
+ Two-factor authentication (2FA)
+
+
+
+
+
+
+ App passwords
+
+ {appPasswords && appPasswords.length > 0 && (
+
+ {appPasswords.length}
+
+ )}
+
+
+
+
+
+ Logged-out visibility
+
+
+
+
+
+
+
+
+
+
+ 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.
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/screens/Settings/Settings.tsx b/src/screens/Settings/Settings.tsx
new file mode 100644
index 0000000000..789ffb56f1
--- /dev/null
+++ b/src/screens/Settings/Settings.tsx
@@ -0,0 +1,282 @@
+import React from 'react'
+import {View} from 'react-native'
+import {Linking} from 'react-native'
+import {AppBskyActorDefs, moderateProfile} from '@atproto/api'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {NativeStackScreenProps} from '@react-navigation/native-stack'
+
+import {HELP_DESK_URL} from '#/lib/constants'
+import {CommonNavigatorParams} from '#/lib/routes/types'
+import {useProfileShadow} from '#/state/cache/profile-shadow'
+import {useModerationOpts} from '#/state/preferences/moderation-opts'
+import {useProfileQuery, useProfilesQuery} from '#/state/queries/profile'
+import {useSession, useSessionApi} from '#/state/session'
+import {useLoggedOutViewControls} from '#/state/shell/logged-out'
+import {useCloseAllActiveElements} from '#/state/util'
+import {UserAvatar} from '#/view/com/util/UserAvatar'
+import {ProfileHeaderDisplayName} from '#/screens/Profile/Header/DisplayName'
+import {ProfileHeaderHandle} from '#/screens/Profile/Header/Handle'
+import * as SettingsList from '#/screens/Settings/components/SettingsList'
+import {atoms as a, useTheme} from '#/alf'
+import {useDialogControl} from '#/components/Dialog'
+import {SwitchAccountDialog} from '#/components/dialogs/SwitchAccount'
+import {Accessibility_Stroke2_Corner2_Rounded as AccessibilityIcon} from '#/components/icons/Accessibility'
+import {BubbleInfo_Stroke2_Corner2_Rounded as BubbleInfoIcon} from '#/components/icons/BubbleInfo'
+import {CircleQuestion_Stroke2_Corner2_Rounded as CircleQuestionIcon} from '#/components/icons/CircleQuestion'
+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,
+} 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 * as Prompt from '#/components/Prompt'
+
+type Props = NativeStackScreenProps
+export function SettingsScreen({}: Props) {
+ const {_} = useLingui()
+ const {logoutEveryAccount} = useSessionApi()
+ const {accounts, currentAccount} = useSession()
+ const switchAccountControl = useDialogControl()
+ const signOutPromptControl = Prompt.usePromptControl()
+ const {data: profile} = useProfileQuery({did: currentAccount?.did})
+ const {setShowLoggedOut} = useLoggedOutViewControls()
+ const closeEverything = useCloseAllActiveElements()
+
+ const onAddAnotherAccount = () => {
+ setShowLoggedOut(true)
+ closeEverything()
+ }
+
+ return (
+
+
+
+
+
+ {profile && }
+
+ 1
+ ? _(msg`Switch account`)
+ : _(msg`Add another account`)
+ }
+ onPress={() =>
+ accounts.length > 1
+ ? switchAccountControl.open()
+ : onAddAnotherAccount()
+ }>
+
+
+ {accounts.length > 1 ? (
+ Switch account
+ ) : (
+ Add another account
+ )}
+
+ {accounts.length > 1 && (
+ acc.did)
+ .filter(did => did !== currentAccount?.did)
+ .slice(0, 5)}
+ />
+ )}
+
+
+
+
+
+ Account
+
+
+
+
+
+ Privacy and security
+
+
+
+
+
+ Moderation
+
+
+
+
+
+ Content and media
+
+
+
+
+
+ Appearance
+
+
+
+
+
+ Accessibility
+
+
+
+
+
+ Languages
+
+
+ Linking.openURL(HELP_DESK_URL)}
+ label={_(msg`Help`)}
+ accessibilityHint={_(msg`Open helpdesk in browser`)}>
+
+
+ Help
+
+
+
+
+
+
+ About
+
+
+
+ signOutPromptControl.open()}
+ label={_(msg`Sign out`)}>
+
+ Sign out
+
+
+
+
+
+ logoutEveryAccount('Settings')}
+ confirmButtonCta={_(msg`Sign out`)}
+ cancelButtonCta={_(msg`Cancel`)}
+ confirmButtonColor="negative"
+ />
+
+
+
+ )
+}
+
+function ProfilePreview({
+ profile,
+}: {
+ profile: AppBskyActorDefs.ProfileViewDetailed
+}) {
+ const shadow = useProfileShadow(profile)
+ const moderationOpts = useModerationOpts()
+
+ if (!moderationOpts) return null
+
+ const moderation = moderateProfile(profile, moderationOpts)
+
+ return (
+ <>
+
+
+
+ >
+ )
+}
+
+const AVI_SIZE = 26
+const HALF_AVI_SIZE = AVI_SIZE / 2
+
+function AvatarStack({profiles}: {profiles: string[]}) {
+ const {data, error} = useProfilesQuery({handles: profiles})
+ const t = useTheme()
+ const moderationOpts = useModerationOpts()
+
+ if (error) {
+ console.error(error)
+ return null
+ }
+
+ const isPending = !data || !moderationOpts
+
+ const items = isPending
+ ? Array.from({length: profiles.length}).map((_, i) => ({
+ key: i,
+ profile: null,
+ moderation: null,
+ }))
+ : data.profiles.map(item => ({
+ key: item.did,
+ profile: item,
+ moderation: moderateProfile(item, moderationOpts),
+ }))
+
+ return (
+
+ {items.map((item, i) => (
+
+ {item.profile && (
+
+ )}
+
+ ))}
+
+ )
+}
diff --git a/src/screens/Settings/components/Email2FAToggle.tsx b/src/screens/Settings/components/Email2FAToggle.tsx
new file mode 100644
index 0000000000..d89e5f18e4
--- /dev/null
+++ b/src/screens/Settings/components/Email2FAToggle.tsx
@@ -0,0 +1,66 @@
+import React from 'react'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {useModalControls} from '#/state/modals'
+import {useAgent, useSession} from '#/state/session'
+import {DisableEmail2FADialog} from '#/view/screens/Settings/DisableEmail2FADialog'
+import {useDialogControl} from '#/components/Dialog'
+import * as Prompt from '#/components/Prompt'
+import * as SettingsList from './SettingsList'
+
+export function Email2FAToggle() {
+ const {_} = useLingui()
+ const {currentAccount} = useSession()
+ const {openModal} = useModalControls()
+ const disableDialogControl = useDialogControl()
+ const enableDialogControl = useDialogControl()
+ const agent = useAgent()
+
+ const enableEmailAuthFactor = React.useCallback(async () => {
+ if (currentAccount?.email) {
+ await agent.com.atproto.server.updateEmail({
+ email: currentAccount.email,
+ emailAuthFactor: true,
+ })
+ await agent.resumeSession(agent.session!)
+ }
+ }, [currentAccount, agent])
+
+ const onToggle = React.useCallback(() => {
+ if (!currentAccount) {
+ return
+ }
+ if (currentAccount.emailAuthFactor) {
+ disableDialogControl.open()
+ } else {
+ if (!currentAccount.emailConfirmed) {
+ openModal({
+ name: 'verify-email',
+ onSuccess: enableDialogControl.open,
+ })
+ return
+ }
+ enableDialogControl.open()
+ }
+ }, [currentAccount, enableDialogControl, openModal, disableDialogControl])
+
+ return (
+ <>
+
+
+
+ >
+ )
+}
diff --git a/src/screens/Settings/components/PwiOptOut.tsx b/src/screens/Settings/components/PwiOptOut.tsx
new file mode 100644
index 0000000000..4339ade9be
--- /dev/null
+++ b/src/screens/Settings/components/PwiOptOut.tsx
@@ -0,0 +1,100 @@
+import React from 'react'
+import {View} from 'react-native'
+import {ComAtprotoLabelDefs} from '@atproto/api'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {
+ useProfileQuery,
+ useProfileUpdateMutation,
+} from '#/state/queries/profile'
+import {useSession} from '#/state/session'
+import {atoms as a, useTheme} from '#/alf'
+import * as Toggle from '#/components/forms/Toggle'
+import {Text} from '#/components/Typography'
+
+export 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
+
+
+
+
+
+
+
+ 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.
+
+
+
+ )
+}
diff --git a/src/screens/Settings/components/SettingsList.tsx b/src/screens/Settings/components/SettingsList.tsx
new file mode 100644
index 0000000000..86f8040afd
--- /dev/null
+++ b/src/screens/Settings/components/SettingsList.tsx
@@ -0,0 +1,300 @@
+import React, {useContext, useMemo} from 'react'
+import {GestureResponderEvent, StyleProp, View, ViewStyle} from 'react-native'
+
+import {HITSLOP_10} from '#/lib/constants'
+import {atoms as a, useTheme} from '#/alf'
+import * as Button from '#/components/Button'
+import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRightIcon} from '#/components/icons/Chevron'
+import {Link, LinkProps} from '#/components/Link'
+import {createPortalGroup} from '#/components/Portal'
+import {Text} from '#/components/Typography'
+
+const ItemContext = React.createContext({
+ destructive: false,
+ withinGroup: false,
+})
+
+const Portal = createPortalGroup()
+
+export function Container({children}: {children: React.ReactNode}) {
+ return {children}
+}
+
+/**
+ * This uses `Portal` magic ✨ to render the icons and title correctly. ItemIcon and ItemText components
+ * get teleported to the top row, leaving the rest of the children in the bottom row.
+ */
+export function Group({
+ children,
+ destructive = false,
+ iconInset = true,
+ style,
+ contentContainerStyle,
+}: {
+ children: React.ReactNode
+ destructive?: boolean
+ iconInset?: boolean
+ style?: StyleProp
+ contentContainerStyle?: StyleProp
+}) {
+ const context = useMemo(
+ () => ({destructive, withinGroup: true}),
+ [destructive],
+ )
+ return (
+
+
+
+ -
+
+
+ -
+ {children}
+
+
+
+
+ )
+}
+
+export function Item({
+ children,
+ destructive,
+ iconInset = false,
+ style,
+}: {
+ children?: React.ReactNode
+ destructive?: boolean
+ /**
+ * Adds left padding so that the content will be aligned with other Items that contain icons
+ * @default false
+ */
+ iconInset?: boolean
+ style?: StyleProp
+}) {
+ const context = useContext(ItemContext)
+ const childContext = useMemo(() => {
+ if (typeof destructive !== 'boolean') return context
+ return {...context, destructive}
+ }, [context, destructive])
+ return (
+
+
+ {children}
+
+
+ )
+}
+
+export function LinkItem({
+ children,
+ destructive = false,
+ contentContainerStyle,
+ chevronColor,
+ ...props
+}: LinkProps & {
+ contentContainerStyle?: StyleProp
+ destructive?: boolean
+ chevronColor?: string
+}) {
+ const t = useTheme()
+
+ return (
+
+ {args => (
+ -
+ {typeof children === 'function' ? children(args) : children}
+
+
+ )}
+
+ )
+}
+
+export function PressableItem({
+ children,
+ destructive = false,
+ contentContainerStyle,
+ hoverStyle,
+ ...props
+}: Button.ButtonProps & {
+ contentContainerStyle?: StyleProp
+ destructive?: boolean
+}) {
+ const t = useTheme()
+ return (
+
+ {args => (
+ -
+ {typeof children === 'function' ? children(args) : children}
+
+ )}
+
+ )
+}
+
+export function ItemIcon({
+ icon: Comp,
+ size = 'xl',
+ color: colorProp,
+}: Omit, 'position'> & {
+ color?: string
+}) {
+ const t = useTheme()
+ const {destructive, withinGroup} = useContext(ItemContext)
+
+ /*
+ * Copied here from icons/common.tsx so we can tweak if we need to, but
+ * also so that we can calculate transforms.
+ */
+ const iconSize = {
+ xs: 12,
+ sm: 16,
+ md: 20,
+ lg: 24,
+ xl: 28,
+ '2xl': 32,
+ }[size]
+
+ const color =
+ colorProp ?? (destructive ? t.palette.negative_500 : t.atoms.text.color)
+
+ const content = (
+
+
+
+ )
+
+ if (withinGroup) {
+ return {content}
+ } else {
+ return content
+ }
+}
+
+export function ItemText({
+ // eslint-disable-next-line react/prop-types
+ style,
+ ...props
+}: React.ComponentProps) {
+ const t = useTheme()
+ const {destructive, withinGroup} = useContext(ItemContext)
+
+ const content = (
+
+ )
+
+ if (withinGroup) {
+ return {content}
+ } else {
+ return content
+ }
+}
+
+export function Divider() {
+ const t = useTheme()
+ return (
+
+ )
+}
+
+export function Chevron({color: colorProp}: {color?: string}) {
+ const {destructive} = useContext(ItemContext)
+ const t = useTheme()
+ const color =
+ colorProp ?? (destructive ? t.palette.negative_500 : t.palette.contrast_500)
+ return
+}
+
+export function BadgeText({children}: {children: React.ReactNode}) {
+ const t = useTheme()
+ return (
+
+ {children}
+
+ )
+}
+
+export function BadgeButton({
+ label,
+ onPress,
+}: {
+ label: string
+ onPress: (evt: GestureResponderEvent) => void
+}) {
+ const t = useTheme()
+ return (
+
+ {({pressed}) => (
+
+ {label}
+
+ )}
+
+ )
+}
diff --git a/src/view/com/modals/DeleteAccount.tsx b/src/view/com/modals/DeleteAccount.tsx
index 6dd248ca7e..b865d7bbf1 100644
--- a/src/view/com/modals/DeleteAccount.tsx
+++ b/src/view/com/modals/DeleteAccount.tsx
@@ -10,15 +10,15 @@ import {LinearGradient} from 'expo-linear-gradient'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
+import {usePalette} from '#/lib/hooks/usePalette'
+import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
+import {cleanError} from '#/lib/strings/errors'
+import {colors, gradients, s} from '#/lib/styles'
+import {useTheme} from '#/lib/ThemeContext'
+import {isAndroid, isWeb} from '#/platform/detection'
import {useModalControls} from '#/state/modals'
import {DM_SERVICE_HEADERS} from '#/state/queries/messages/const'
import {useAgent, useSession, useSessionApi} from '#/state/session'
-import {usePalette} from 'lib/hooks/usePalette'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {cleanError} from 'lib/strings/errors'
-import {colors, gradients, s} from 'lib/styles'
-import {useTheme} from 'lib/ThemeContext'
-import {isAndroid, isWeb} from 'platform/detection'
import {DeactivateAccountDialog} from '#/screens/Settings/components/DeactivateAccountDialog'
import {atoms as a, useTheme as useNewTheme} from '#/alf'
import {useDialogControl} from '#/components/Dialog'
@@ -210,6 +210,7 @@ export function Component({}: {}) {
to="#"
onPress={e => {
e.preventDefault()
+ closeModal()
deactivateAccountControl.open()
return false
}}>
diff --git a/src/view/screens/AccessibilitySettings.tsx b/src/view/screens/AccessibilitySettings.tsx
index bf9f5fcb5e..4dd5aa97be 100644
--- a/src/view/screens/AccessibilitySettings.tsx
+++ b/src/view/screens/AccessibilitySettings.tsx
@@ -4,6 +4,7 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native'
+import {IS_INTERNAL} from '#/lib/app-info'
import {usePalette} from '#/lib/hooks/usePalette'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
@@ -26,6 +27,7 @@ import {ToggleButton} from '#/view/com/util/forms/ToggleButton'
import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader'
import {Text} from '#/view/com/util/text/Text'
import {ScrollView} from '#/view/com/util/Views'
+import {AccessibilitySettingsScreen as NewAccessibilitySettingsScreen} from '#/screens/Settings/AccessibilitySettings'
import {atoms as a} from '#/alf'
import * as Layout from '#/components/Layout'
@@ -33,7 +35,15 @@ type Props = NativeStackScreenProps<
CommonNavigatorParams,
'AccessibilitySettings'
>
-export function AccessibilitySettingsScreen({}: Props) {
+export function AccessibilitySettingsScreen(props: Props) {
+ return IS_INTERNAL ? (
+
+ ) : (
+
+ )
+}
+
+function LegacyAccessibilitySettingsScreen({}: Props) {
const pal = usePalette('default')
const setMinimalShellMode = useSetMinimalShellMode()
const {isMobile, isTabletOrMobile} = useWebMediaQueries()
diff --git a/src/view/screens/Settings/DisableEmail2FADialog.tsx b/src/view/screens/Settings/DisableEmail2FADialog.tsx
index e4341fcd21..e3d0885177 100644
--- a/src/view/screens/Settings/DisableEmail2FADialog.tsx
+++ b/src/view/screens/Settings/DisableEmail2FADialog.tsx
@@ -108,7 +108,11 @@ export function DisableEmail2FADialog({
{error ? : undefined}
{stage === Stages.Email ? (
-
+
-
+