diff --git a/assets/icons/beaker_stroke2_corner2_rounded.svg b/assets/icons/beaker_stroke2_corner2_rounded.svg
new file mode 100644
index 0000000000..1ecae4f50c
--- /dev/null
+++ b/assets/icons/beaker_stroke2_corner2_rounded.svg
@@ -0,0 +1 @@
+
diff --git a/src/Navigation.tsx b/src/Navigation.tsx
index 436f2488a5..efe4b8c292 100644
--- a/src/Navigation.tsx
+++ b/src/Navigation.tsx
@@ -55,7 +55,6 @@ import {ModerationModlistsScreen} from '#/view/screens/ModerationModlists'
import {ModerationMutedAccounts} from '#/view/screens/ModerationMutedAccounts'
import {NotFoundScreen} from '#/view/screens/NotFound'
import {NotificationsScreen} from '#/view/screens/Notifications'
-import {NotificationsSettingsScreen} from '#/view/screens/NotificationsSettings'
import {PostThreadScreen} from '#/view/screens/PostThread'
import {PreferencesExternalEmbeds} from '#/view/screens/PreferencesExternalEmbeds'
import {PreferencesFollowingFeed} from '#/view/screens/PreferencesFollowingFeed'
@@ -87,6 +86,7 @@ import {PostRepostedByScreen} from '#/screens/Post/PostRepostedBy'
import {ProfileKnownFollowersScreen} from '#/screens/Profile/KnownFollowers'
import {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLikedBy'
import {AppearanceSettingsScreen} from '#/screens/Settings/AppearanceSettings'
+import {NotificationSettingsScreen} from '#/screens/Settings/NotificationSettings'
import {
StarterPackScreen,
StarterPackScreenShort,
@@ -378,8 +378,8 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
options={{title: title(msg`Chat settings`), requireAuth: true}}
/>
NotificationsSettingsScreen}
+ name="NotificationSettings"
+ getComponent={() => NotificationSettingsScreen}
options={{title: title(msg`Notification settings`), requireAuth: true}}
/>
@@ -32,10 +30,9 @@ 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 changeHandleControl = useDialogControl()
const exportCarControl = useDialogControl()
const deactivateAccountControl = useDialogControl()
@@ -117,26 +114,12 @@ export function AccountSettingsScreen({}: Props) {
- openModal({
- name: 'change-handle',
- onChanged() {
- if (currentAccount) {
- // refresh my profile
- queryClient.invalidateQueries({
- queryKey: RQKEY_PROFILE(currentAccount.did),
- })
- }
- },
- })
- }>
+ accessibilityHint={_(msg`Open change handle dialog`)}
+ onPress={() => changeHandleControl.open()}>
Handle
- {profile && (
- @{profile.handle}
- )}
@@ -173,6 +156,7 @@ export function AccountSettingsScreen({}: Props) {
+
diff --git a/src/screens/Settings/AppPasswords.tsx b/src/screens/Settings/AppPasswords.tsx
new file mode 100644
index 0000000000..8cebf97ce6
--- /dev/null
+++ b/src/screens/Settings/AppPasswords.tsx
@@ -0,0 +1,209 @@
+import React, {useCallback} from 'react'
+import {View} from 'react-native'
+import Animated, {
+ FadeIn,
+ FadeOut,
+ LayoutAnimationConfig,
+ LinearTransition,
+ StretchOutY,
+} from 'react-native-reanimated'
+import {ComAtprotoServerListAppPasswords} from '@atproto/api'
+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 {cleanError} from '#/lib/strings/errors'
+import {isWeb} from '#/platform/detection'
+import {
+ useAppPasswordDeleteMutation,
+ useAppPasswordsQuery,
+} from '#/state/queries/app-passwords'
+import {EmptyState} from '#/view/com/util/EmptyState'
+import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
+import * as Toast from '#/view/com/util/Toast'
+import {atoms as a, useTheme} from '#/alf'
+import {Admonition, colors} from '#/components/Admonition'
+import {Button, ButtonIcon, ButtonText} from '#/components/Button'
+import {useDialogControl} from '#/components/Dialog'
+import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus'
+import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash'
+import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
+import * as Layout from '#/components/Layout'
+import {Loader} from '#/components/Loader'
+import * as Prompt from '#/components/Prompt'
+import {Text} from '#/components/Typography'
+import {AddAppPasswordDialog} from './components/AddAppPasswordDialog'
+import * as SettingsList from './components/SettingsList'
+
+type Props = NativeStackScreenProps
+export function AppPasswordsScreen({}: Props) {
+ const {_} = useLingui()
+ const {data: appPasswords, error} = useAppPasswordsQuery()
+ const createAppPasswordControl = useDialogControl()
+
+ return (
+
+
+
+ {error ? (
+
+ ) : (
+
+
+
+
+ Use app passwords to sign in to other Bluesky clients without
+ giving full access to your account or password.
+
+
+
+
+
+
+
+
+ {appPasswords ? (
+ appPasswords.length > 0 ? (
+
+ {appPasswords.map(appPassword => (
+
+
+
+
+
+ ))}
+
+ ) : (
+
+ )
+ ) : (
+
+
+
+ )}
+
+
+ )}
+
+
+ p.name) || []}
+ />
+
+ )
+}
+
+function AppPasswordCard({
+ appPassword,
+}: {
+ appPassword: ComAtprotoServerListAppPasswords.AppPassword
+}) {
+ const t = useTheme()
+ const {i18n, _} = useLingui()
+ const deleteControl = Prompt.usePromptControl()
+ const {mutateAsync: deleteMutation} = useAppPasswordDeleteMutation()
+
+ const onDelete = useCallback(async () => {
+ await deleteMutation({name: appPassword.name})
+ Toast.show(_(msg`App password deleted`))
+ }, [deleteMutation, appPassword.name, _])
+
+ return (
+
+
+
+
+ {appPassword.name}
+
+
+
+ Created{' '}
+ {i18n.date(appPassword.createdAt, {
+ year: 'numeric',
+ month: 'numeric',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit',
+ })}
+
+
+
+
+
+ {appPassword.privileged && (
+
+
+
+ Allows access to direct messages
+
+
+ )}
+
+
+
+ )
+}
diff --git a/src/screens/Settings/ExternalMediaPreferences.tsx b/src/screens/Settings/ExternalMediaPreferences.tsx
new file mode 100644
index 0000000000..91c7ea7fc7
--- /dev/null
+++ b/src/screens/Settings/ExternalMediaPreferences.tsx
@@ -0,0 +1,99 @@
+import React, {Fragment} from 'react'
+import {View} from 'react-native'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
+import {
+ EmbedPlayerSource,
+ externalEmbedLabels,
+} from '#/lib/strings/embed-player'
+import {
+ useExternalEmbedsPrefs,
+ useSetExternalEmbedPref,
+} from '#/state/preferences'
+import {atoms as a, native} from '#/alf'
+import {Admonition} from '#/components/Admonition'
+import * as Toggle from '#/components/forms/Toggle'
+import * as Layout from '#/components/Layout'
+import * as SettingsList from './components/SettingsList'
+
+type Props = NativeStackScreenProps<
+ CommonNavigatorParams,
+ 'PreferencesExternalEmbeds'
+>
+export function ExternalMediaPreferencesScreen({}: Props) {
+ const {_} = useLingui()
+ return (
+
+
+
+
+
+
+
+ External media may allow websites to collect information about
+ you and your device. No information is sent or requested until
+ you press the "play" button.
+
+
+
+
+
+ Enable media players for
+
+
+ {native()}
+ {Object.entries(externalEmbedLabels)
+ // TODO: Remove special case when we disable the old integration.
+ .filter(([key]) => key !== 'tenor')
+ .map(([key, label]) => (
+
+
+ {native()}
+
+ ))}
+
+
+
+
+
+ )
+}
+
+function PrefSelector({
+ source,
+ label,
+}: {
+ source: EmbedPlayerSource
+ label: string
+}) {
+ const setExternalEmbedPref = useSetExternalEmbedPref()
+ const sources = useExternalEmbedsPrefs()
+
+ return (
+
+ setExternalEmbedPref(
+ source,
+ sources?.[source] === 'show' ? 'hide' : 'show',
+ )
+ }
+ style={[
+ a.flex_1,
+ a.py_md,
+ native([a.justify_between, a.flex_row_reverse]),
+ ]}>
+
+ {label}
+
+ )
+}
diff --git a/src/screens/Settings/FollowingFeedPreferences.tsx b/src/screens/Settings/FollowingFeedPreferences.tsx
new file mode 100644
index 0000000000..12de2a31a7
--- /dev/null
+++ b/src/screens/Settings/FollowingFeedPreferences.tsx
@@ -0,0 +1,143 @@
+import React from 'react'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
+import {
+ usePreferencesQuery,
+ useSetFeedViewPreferencesMutation,
+} from '#/state/queries/preferences'
+import {atoms as a} from '#/alf'
+import {Admonition} from '#/components/Admonition'
+import * as Toggle from '#/components/forms/Toggle'
+import {Beaker_Stroke2_Corner2_Rounded as BeakerIcon} from '#/components/icons/Beaker'
+import {Bubbles_Stroke2_Corner2_Rounded as BubblesIcon} from '#/components/icons/Bubble'
+import {CloseQuote_Stroke2_Corner1_Rounded as QuoteIcon} from '#/components/icons/Quote'
+import {Repost_Stroke2_Corner2_Rounded as RepostIcon} from '#/components/icons/Repost'
+import * as Layout from '#/components/Layout'
+import * as SettingsList from './components/SettingsList'
+
+type Props = NativeStackScreenProps<
+ CommonNavigatorParams,
+ 'PreferencesFollowingFeed'
+>
+export function FollowingFeedPreferencesScreen({}: Props) {
+ const {_} = useLingui()
+
+ const {data: preferences} = usePreferencesQuery()
+ const {mutate: setFeedViewPref, variables} =
+ useSetFeedViewPreferencesMutation()
+
+ const showReplies = !(
+ variables?.hideReplies ?? preferences?.feedViewPrefs?.hideReplies
+ )
+
+ const showReposts = !(
+ variables?.hideReposts ?? preferences?.feedViewPrefs?.hideReposts
+ )
+
+ const showQuotePosts = !(
+ variables?.hideQuotePosts ?? preferences?.feedViewPrefs?.hideQuotePosts
+ )
+
+ const mergeFeedEnabled = Boolean(
+ variables?.lab_mergeFeedEnabled ??
+ preferences?.feedViewPrefs?.lab_mergeFeedEnabled,
+ )
+
+ return (
+
+
+
+
+
+
+ These settings only apply to the Following feed.
+
+
+
+ setFeedViewPref({
+ hideReplies: !value,
+ })
+ }>
+
+
+
+ Show replies
+
+
+
+
+
+ setFeedViewPref({
+ hideReposts: !value,
+ })
+ }>
+
+
+
+ Show reposts
+
+
+
+
+
+ setFeedViewPref({
+ hideQuotePosts: !value,
+ })
+ }>
+
+
+
+ Show quote posts
+
+
+
+
+
+
+
+
+ Experimental
+
+
+ setFeedViewPref({
+ lab_mergeFeedEnabled: value,
+ })
+ }
+ style={[a.w_full, a.gap_md]}>
+
+
+ Show samples of your saved feeds in your Following feed
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/screens/Settings/LanguageSettings.tsx b/src/screens/Settings/LanguageSettings.tsx
new file mode 100644
index 0000000000..c6cd8bb5a6
--- /dev/null
+++ b/src/screens/Settings/LanguageSettings.tsx
@@ -0,0 +1,275 @@
+import React, {useCallback, useMemo} from 'react'
+import {View} from 'react-native'
+import RNPickerSelect, {PickerSelectProps} from 'react-native-picker-select'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {APP_LANGUAGES, LANGUAGES} from '#/lib/../locale/languages'
+import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
+import {sanitizeAppLanguageSetting} from '#/locale/helpers'
+import {useModalControls} from '#/state/modals'
+import {useLanguagePrefs, useLanguagePrefsApi} from '#/state/preferences'
+import {atoms as a, useTheme, web} from '#/alf'
+import {Button, ButtonIcon, ButtonText} from '#/components/Button'
+import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
+import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDownIcon} from '#/components/icons/Chevron'
+import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus'
+import * as Layout from '#/components/Layout'
+import {Text} from '#/components/Typography'
+import * as SettingsList from './components/SettingsList'
+
+type Props = NativeStackScreenProps
+export function LanguageSettingsScreen({}: Props) {
+ const {_} = useLingui()
+ const langPrefs = useLanguagePrefs()
+ const setLangPrefs = useLanguagePrefsApi()
+ const t = useTheme()
+
+ const {openModal} = useModalControls()
+
+ const onPressContentLanguages = useCallback(() => {
+ openModal({name: 'content-languages-settings'})
+ }, [openModal])
+
+ const onChangePrimaryLanguage = useCallback(
+ (value: Parameters[0]) => {
+ if (!value) return
+ if (langPrefs.primaryLanguage !== value) {
+ setLangPrefs.setPrimaryLanguage(value)
+ }
+ },
+ [langPrefs, setLangPrefs],
+ )
+
+ const onChangeAppLanguage = useCallback(
+ (value: Parameters[0]) => {
+ if (!value) return
+ if (langPrefs.appLanguage !== value) {
+ setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value))
+ }
+ },
+ [langPrefs, setLangPrefs],
+ )
+
+ const myLanguages = useMemo(() => {
+ return (
+ langPrefs.contentLanguages
+ .map(lang => LANGUAGES.find(l => l.code2 === lang))
+ .filter(Boolean)
+ // @ts-ignore
+ .map(l => l.name)
+ .join(', ')
+ )
+ }, [langPrefs.contentLanguages])
+
+ return (
+
+
+
+
+
+
+ App Language
+
+
+
+
+ Select your app language for the default text to display in
+ the app.
+
+
+
+ Boolean(l.code2)).map(l => ({
+ label: l.name,
+ value: l.code2,
+ key: l.code2,
+ }))}
+ style={{
+ inputAndroid: {
+ backgroundColor: t.atoms.bg_contrast_25.backgroundColor,
+ color: t.atoms.text.color,
+ fontSize: 14,
+ letterSpacing: 0.5,
+ fontWeight: a.font_bold.fontWeight,
+ paddingHorizontal: 14,
+ paddingVertical: 8,
+ borderRadius: a.rounded_xs.borderRadius,
+ },
+ inputIOS: {
+ backgroundColor: t.atoms.bg_contrast_25.backgroundColor,
+ color: t.atoms.text.color,
+ fontSize: 14,
+ letterSpacing: 0.5,
+ fontWeight: a.font_bold.fontWeight,
+ paddingHorizontal: 14,
+ paddingVertical: 8,
+ borderRadius: a.rounded_xs.borderRadius,
+ },
+ inputWeb: {
+ flex: 1,
+ width: '100%',
+ cursor: 'pointer',
+ // @ts-ignore web only
+ '-moz-appearance': 'none',
+ '-webkit-appearance': 'none',
+ appearance: 'none',
+ outline: 0,
+ borderWidth: 0,
+ backgroundColor: t.atoms.bg_contrast_25.backgroundColor,
+ color: t.atoms.text.color,
+ fontSize: 14,
+ fontFamily: 'inherit',
+ letterSpacing: 0.5,
+ fontWeight: a.font_bold.fontWeight,
+ paddingHorizontal: 14,
+ paddingVertical: 8,
+ borderRadius: a.rounded_xs.borderRadius,
+ },
+ }}
+ />
+
+
+
+
+
+
+
+
+
+
+ Primary Language
+
+
+
+
+ Select your preferred language for translations in your feed.
+
+
+
+ Boolean(l.code2)).map(l => ({
+ label: l.name,
+ value: l.code2,
+ key: l.code2 + l.code3,
+ }))}
+ style={{
+ inputAndroid: {
+ backgroundColor: t.atoms.bg_contrast_25.backgroundColor,
+ color: t.atoms.text.color,
+ fontSize: 14,
+ letterSpacing: 0.5,
+ fontWeight: a.font_bold.fontWeight,
+ paddingHorizontal: 14,
+ paddingVertical: 8,
+ borderRadius: a.rounded_xs.borderRadius,
+ },
+ inputIOS: {
+ backgroundColor: t.atoms.bg_contrast_25.backgroundColor,
+ color: t.atoms.text.color,
+ fontSize: 14,
+ letterSpacing: 0.5,
+ fontWeight: a.font_bold.fontWeight,
+ paddingHorizontal: 14,
+ paddingVertical: 8,
+ borderRadius: a.rounded_xs.borderRadius,
+ },
+ inputWeb: {
+ flex: 1,
+ width: '100%',
+ cursor: 'pointer',
+ // @ts-ignore web only
+ '-moz-appearance': 'none',
+ '-webkit-appearance': 'none',
+ appearance: 'none',
+ outline: 0,
+ borderWidth: 0,
+ backgroundColor: t.atoms.bg_contrast_25.backgroundColor,
+ color: t.atoms.text.color,
+ fontSize: 14,
+ fontFamily: 'inherit',
+ letterSpacing: 0.5,
+ fontWeight: a.font_bold.fontWeight,
+ paddingHorizontal: 14,
+ paddingVertical: 8,
+ borderRadius: a.rounded_xs.borderRadius,
+ },
+ }}
+ />
+
+
+
+
+
+
+
+
+
+
+ Content Languages
+
+
+
+
+ Select which languages you want your subscribed feeds to
+ include. If none are selected, all languages will be shown.
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/screens/Settings/NotificationSettings.tsx b/src/screens/Settings/NotificationSettings.tsx
new file mode 100644
index 0000000000..8e10337423
--- /dev/null
+++ b/src/screens/Settings/NotificationSettings.tsx
@@ -0,0 +1,85 @@
+import React from 'react'
+import {Text} from 'react-native'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {AllNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
+import {useNotificationFeedQuery} from '#/state/queries/notifications/feed'
+import {useNotificationSettingsMutation} from '#/state/queries/notifications/settings'
+import {atoms as a} from '#/alf'
+import {Admonition} from '#/components/Admonition'
+import {Error} from '#/components/Error'
+import * as Toggle from '#/components/forms/Toggle'
+import {Beaker_Stroke2_Corner2_Rounded as BeakerIcon} from '#/components/icons/Beaker'
+import * as Layout from '#/components/Layout'
+import {Loader} from '#/components/Loader'
+import * as SettingsList from './components/SettingsList'
+
+type Props = NativeStackScreenProps
+export function NotificationSettingsScreen({}: Props) {
+ const {_} = useLingui()
+
+ const {data, isError: isQueryError, refetch} = useNotificationFeedQuery()
+ const serverPriority = data?.pages.at(0)?.priority
+
+ const {
+ mutate: onChangePriority,
+ isPending: isMutationPending,
+ variables,
+ } = useNotificationSettingsMutation()
+
+ const priority = isMutationPending
+ ? variables[0] === 'enabled'
+ : serverPriority
+
+ return (
+
+
+
+ {isQueryError ? (
+
+ ) : (
+
+
+
+
+ Notification filters
+
+
+
+
+ Enable priority notifications
+
+ {!data ? : }
+
+
+
+
+
+
+ Experimental: When this
+ preference is enabled, you'll only receive reply and quote
+ notifications from users you follow. We'll continue to add
+ more controls here over time.
+
+
+
+
+ )}
+
+
+ )
+}
diff --git a/src/screens/Settings/Settings.tsx b/src/screens/Settings/Settings.tsx
index 789ffb56f1..bfdb79135e 100644
--- a/src/screens/Settings/Settings.tsx
+++ b/src/screens/Settings/Settings.tsx
@@ -60,6 +60,7 @@ export function SettingsScreen({}: Props) {
+export function ThreadPreferencesScreen({}: Props) {
+ const {_} = useLingui()
+ const t = useTheme()
+
+ const {data: preferences} = usePreferencesQuery()
+ const {mutate: setThreadViewPrefs, variables} =
+ useSetThreadViewPreferencesMutation()
+
+ const sortReplies = variables?.sort ?? preferences?.threadViewPrefs?.sort
+
+ const prioritizeFollowedUsers = Boolean(
+ variables?.prioritizeFollowedUsers ??
+ preferences?.threadViewPrefs?.prioritizeFollowedUsers,
+ )
+ const treeViewEnabled = Boolean(
+ variables?.lab_treeViewEnabled ??
+ preferences?.threadViewPrefs?.lab_treeViewEnabled,
+ )
+
+ return (
+
+
+
+
+
+
+ Sort replies
+
+
+ Sort replies to the same post by:
+
+ setThreadViewPrefs({sort: values[0]})}>
+
+
+
+
+ Oldest replies first
+
+
+
+
+
+ Newest replies first
+
+
+
+
+
+ Most-liked first
+
+
+
+
+
+ Random (aka "Poster's Roulette")
+
+
+
+
+
+
+
+
+
+ Prioritize your Follows
+
+
+ setThreadViewPrefs({
+ prioritizeFollowedUsers: value,
+ })
+ }
+ style={[a.w_full, a.gap_md]}>
+
+
+ Show replies by people you follow before all other replies.
+
+
+
+
+
+
+
+
+
+ Experimental
+
+
+ setThreadViewPrefs({
+ lab_treeViewEnabled: value,
+ })
+ }
+ style={[a.w_full, a.gap_md]}>
+
+ Show replies in a threaded view
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/screens/Settings/components/AddAppPasswordDialog.tsx b/src/screens/Settings/components/AddAppPasswordDialog.tsx
new file mode 100644
index 0000000000..dcb2128792
--- /dev/null
+++ b/src/screens/Settings/components/AddAppPasswordDialog.tsx
@@ -0,0 +1,280 @@
+import React, {useEffect, useMemo, useState} from 'react'
+import {useWindowDimensions, View} from 'react-native'
+import Animated, {
+ FadeIn,
+ FadeOut,
+ LayoutAnimationConfig,
+ LinearTransition,
+ SlideInRight,
+ SlideOutLeft,
+} from 'react-native-reanimated'
+import {ComAtprotoServerCreateAppPassword} from '@atproto/api'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useMutation} from '@tanstack/react-query'
+
+import {isWeb} from '#/platform/detection'
+import {useAppPasswordCreateMutation} from '#/state/queries/app-passwords'
+import {atoms as a, native, useTheme} from '#/alf'
+import {Admonition} from '#/components/Admonition'
+import {Button, ButtonIcon, ButtonText} from '#/components/Button'
+import * as Dialog from '#/components/Dialog'
+import * as TextInput from '#/components/forms/TextField'
+import * as Toggle from '#/components/forms/Toggle'
+import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
+import {SquareBehindSquare4_Stroke2_Corner0_Rounded as CopyIcon} from '#/components/icons/SquareBehindSquare4'
+import {Text} from '#/components/Typography'
+import {CopyButton} from './CopyButton'
+
+export function AddAppPasswordDialog({
+ control,
+ passwords,
+}: {
+ control: Dialog.DialogControlProps
+ passwords: string[]
+}) {
+ const {height} = useWindowDimensions()
+ return (
+
+
+
+
+ )
+}
+
+function CreateDialogInner({passwords}: {passwords: string[]}) {
+ const control = Dialog.useDialogContext()
+ const t = useTheme()
+ const {_} = useLingui()
+ const autogeneratedName = useRandomName()
+ const [name, setName] = useState('')
+ const [privileged, setPrivileged] = useState(false)
+ const {
+ mutateAsync: actuallyCreateAppPassword,
+ error: apiError,
+ data,
+ } = useAppPasswordCreateMutation()
+
+ const regexFailError = useMemo(
+ () =>
+ new DisplayableError(
+ _(
+ msg`App password names can only contain letters, numbers, spaces, dashes, and underscores`,
+ ),
+ ),
+ [_],
+ )
+
+ const {
+ mutate: createAppPassword,
+ error: validationError,
+ isPending,
+ } = useMutation<
+ ComAtprotoServerCreateAppPassword.AppPassword,
+ Error | DisplayableError
+ >({
+ mutationFn: async () => {
+ const chosenName = name.trim() || autogeneratedName
+ if (chosenName.length < 4) {
+ throw new DisplayableError(
+ _(msg`App password names must be at least 4 characters long`),
+ )
+ }
+ if (passwords.find(p => p === chosenName)) {
+ throw new DisplayableError(_(msg`App password name must be unique`))
+ }
+ return await actuallyCreateAppPassword({name: chosenName, privileged})
+ },
+ })
+
+ const [hasBeenCopied, setHasBeenCopied] = useState(false)
+ useEffect(() => {
+ if (hasBeenCopied) {
+ const timeout = setTimeout(() => setHasBeenCopied(false), 100)
+ return () => clearTimeout(timeout)
+ }
+ }, [hasBeenCopied])
+
+ const error =
+ validationError || (!name.match(/^[a-zA-Z0-9-_ ]*$/) && regexFailError)
+
+ return (
+
+
+
+ {!data ? (
+
+
+ Add App Password
+
+
+
+ Please enter a unique name for this app password or use our
+ randomly generated one.
+
+
+
+
+ createAppPassword()}
+ blurOnSubmit
+ autoCorrect={false}
+ autoComplete="off"
+ autoCapitalize="none"
+ autoFocus
+ />
+
+
+ {error instanceof DisplayableError && (
+
+ {error.message}
+
+ )}
+
+
+
+
+ Allow access to your direct messages
+
+
+
+ {!!apiError ||
+ (error && !(error instanceof DisplayableError) && (
+
+
+
+ Failed to create app password. Please try again.
+
+
+
+ ))}
+
+
+ ) : (
+
+
+ Here is your app password!
+
+
+
+ Use this to sign into the other app along with your handle.
+
+
+
+ {data.password}
+
+
+
+
+ For security reasons, you won't be able to view this again. If
+ you lose this app password, you'll need to generate a new one.
+
+
+
+
+ )}
+
+
+
+
+ )
+}
+
+class DisplayableError extends Error {
+ constructor(message: string) {
+ super(message)
+ this.name = 'DisplayableError'
+ }
+}
+
+function useRandomName() {
+ return useState(
+ () => shadesOfBlue[Math.floor(Math.random() * shadesOfBlue.length)],
+ )[0]
+}
+
+const shadesOfBlue: string[] = [
+ 'AliceBlue',
+ 'Aqua',
+ 'Aquamarine',
+ 'Azure',
+ 'BabyBlue',
+ 'Blue',
+ 'BlueViolet',
+ 'CadetBlue',
+ 'CornflowerBlue',
+ 'Cyan',
+ 'DarkBlue',
+ 'DarkCyan',
+ 'DarkSlateBlue',
+ 'DeepSkyBlue',
+ 'DodgerBlue',
+ 'ElectricBlue',
+ 'LightBlue',
+ 'LightCyan',
+ 'LightSkyBlue',
+ 'LightSteelBlue',
+ 'MediumAquaMarine',
+ 'MediumBlue',
+ 'MediumSlateBlue',
+ 'MidnightBlue',
+ 'Navy',
+ 'PowderBlue',
+ 'RoyalBlue',
+ 'SkyBlue',
+ 'SlateBlue',
+ 'SteelBlue',
+ 'Teal',
+ 'Turquoise',
+]
diff --git a/src/screens/Settings/components/ChangeHandleDialog.tsx b/src/screens/Settings/components/ChangeHandleDialog.tsx
new file mode 100644
index 0000000000..e76d6257fe
--- /dev/null
+++ b/src/screens/Settings/components/ChangeHandleDialog.tsx
@@ -0,0 +1,602 @@
+import React, {useCallback, useMemo, useState} from 'react'
+import {useWindowDimensions, View} from 'react-native'
+import Animated, {
+ FadeIn,
+ FadeOut,
+ LayoutAnimationConfig,
+ LinearTransition,
+ SlideInLeft,
+ SlideInRight,
+ SlideOutLeft,
+ SlideOutRight,
+} from 'react-native-reanimated'
+import {ComAtprotoServerDescribeServer} from '@atproto/api'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useMutation, useQueryClient} from '@tanstack/react-query'
+
+import {HITSLOP_10} from '#/lib/constants'
+import {cleanError} from '#/lib/strings/errors'
+import {createFullHandle, validateHandle} from '#/lib/strings/handles'
+import {useFetchDid, useUpdateHandleMutation} from '#/state/queries/handle'
+import {RQKEY as RQKEY_PROFILE} from '#/state/queries/profile'
+import {useServiceQuery} from '#/state/queries/service'
+import {useAgent, useSession} from '#/state/session'
+import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
+import {atoms as a, native, useBreakpoints, useTheme} from '#/alf'
+import {Admonition} from '#/components/Admonition'
+import {Button, ButtonIcon, ButtonText} from '#/components/Button'
+import * as Dialog from '#/components/Dialog'
+import * as TextField from '#/components/forms/TextField'
+import * as ToggleButton from '#/components/forms/ToggleButton'
+import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRightIcon} from '#/components/icons/Arrow'
+import {At_Stroke2_Corner0_Rounded as AtIcon} from '#/components/icons/At'
+import {CheckThick_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
+import {SquareBehindSquare4_Stroke2_Corner0_Rounded as CopyIcon} from '#/components/icons/SquareBehindSquare4'
+import {InlineLinkText} from '#/components/Link'
+import {Loader} from '#/components/Loader'
+import {Text} from '#/components/Typography'
+import {CopyButton} from './CopyButton'
+
+export function ChangeHandleDialog({
+ control,
+}: {
+ control: Dialog.DialogControlProps
+}) {
+ const {height} = useWindowDimensions()
+
+ return (
+
+
+
+ )
+}
+
+function ChangeHandleDialogInner() {
+ const control = Dialog.useDialogContext()
+ const {_} = useLingui()
+ const agent = useAgent()
+ const {
+ data: serviceInfo,
+ error: serviceInfoError,
+ refetch,
+ } = useServiceQuery(agent.serviceUrl.toString())
+
+ const [page, setPage] = useState<'provided-handle' | 'own-handle'>(
+ 'provided-handle',
+ )
+
+ const cancelButton = useCallback(
+ () => (
+
+ ),
+ [control, _],
+ )
+
+ return (
+
+
+ Change Handle
+
+
+ }
+ contentContainerStyle={[a.pt_0, a.px_0]}>
+
+ {serviceInfoError ? (
+
+ ) : serviceInfo ? (
+
+ {page === 'provided-handle' ? (
+
+ setPage('own-handle')}
+ />
+
+ ) : (
+
+ setPage('provided-handle')}
+ />
+
+ )}
+
+ ) : (
+
+
+
+ )}
+
+
+ )
+}
+
+function ProvidedHandlePage({
+ serviceInfo,
+ goToOwnHandle,
+}: {
+ serviceInfo: ComAtprotoServerDescribeServer.OutputSchema
+ goToOwnHandle: () => void
+}) {
+ const {_} = useLingui()
+ const [subdomain, setSubdomain] = useState('')
+ const agent = useAgent()
+ const control = Dialog.useDialogContext()
+ const {currentAccount} = useSession()
+ const queryClient = useQueryClient()
+
+ const {
+ mutate: changeHandle,
+ isPending,
+ error,
+ isSuccess,
+ } = useUpdateHandleMutation({
+ onSuccess: () => {
+ if (currentAccount) {
+ queryClient.invalidateQueries({
+ queryKey: RQKEY_PROFILE(currentAccount.did),
+ })
+ }
+ agent.resumeSession(agent.session!).then(() => control.close())
+ },
+ })
+
+ const host = serviceInfo.availableUserDomains[0]
+
+ const validation = useMemo(
+ () => validateHandle(subdomain, host),
+ [subdomain, host],
+ )
+
+ const isTooLong = subdomain.length > 18
+ const isInvalid =
+ isTooLong ||
+ !validation.handleChars ||
+ !validation.hyphenStartOrEnd ||
+ !validation.totalLength
+
+ return (
+
+
+ {isSuccess && (
+
+
+
+ )}
+ {error && (
+
+
+
+ )}
+
+
+
+ New handle
+
+
+
+ setSubdomain(text)}
+ label={_(msg`New handle`)}
+ placeholder={_(msg`e.g. alice`)}
+ autoCapitalize="none"
+ autoCorrect={false}
+ />
+
+ {host}
+
+
+
+
+
+ Your full handle will be{' '}
+
+ @{createFullHandle(subdomain, host)}
+
+
+
+
+
+
+ If you have your own domain, you can use that as your handle. This
+ lets you self-verify your identity –{' '}
+
+ learn more
+
+ .
+
+
+
+
+
+
+ )
+}
+
+function OwnHandlePage({goToServiceHandle}: {goToServiceHandle: () => void}) {
+ const {_} = useLingui()
+ const t = useTheme()
+ const {currentAccount} = useSession()
+ const [dnsPanel, setDNSPanel] = useState(true)
+ const [domain, setDomain] = useState('')
+ const agent = useAgent()
+ const control = Dialog.useDialogContext()
+ const fetchDid = useFetchDid()
+ const queryClient = useQueryClient()
+
+ const {
+ mutate: changeHandle,
+ isPending,
+ error,
+ isSuccess,
+ } = useUpdateHandleMutation({
+ onSuccess: () => {
+ if (currentAccount) {
+ queryClient.invalidateQueries({
+ queryKey: RQKEY_PROFILE(currentAccount.did),
+ })
+ }
+ agent.resumeSession(agent.session!).then(() => control.close())
+ },
+ })
+
+ const {
+ mutate: verify,
+ isPending: isVerifyPending,
+ isSuccess: isVerified,
+ error: verifyError,
+ reset: resetVerification,
+ } = useMutation({
+ mutationKey: ['verify-handle', domain],
+ mutationFn: async () => {
+ const did = await fetchDid(domain)
+ if (did !== currentAccount?.did) {
+ throw new DidMismatchError(did)
+ }
+ return true
+ },
+ })
+
+ return (
+
+ {isSuccess && (
+
+
+
+ )}
+ {error && (
+
+
+
+ )}
+ {verifyError && (
+
+
+ {verifyError instanceof DidMismatchError ? (
+
+ Wrong DID returned from server. Received: {verifyError.did}
+
+ ) : (
+ Failed to verify handle. Please try again.
+ )}
+
+
+ )}
+
+
+
+ Enter the domain you want to use
+
+
+
+ {
+ setDomain(text)
+ resetVerification()
+ }}
+ autoCapitalize="none"
+ autoCorrect={false}
+ />
+
+
+ setDNSPanel(values[0] === 'dns')}>
+
+
+ DNS Panel
+
+
+
+
+ No DNS Panel
+
+
+
+ {dnsPanel ? (
+ <>
+
+ Add the following DNS record to your domain:
+
+
+
+ Host:
+
+
+
+ _atproto
+
+
+
+
+ Type:
+
+
+ TXT
+
+
+ Value:
+
+
+
+
+ did={currentAccount?.did}
+
+
+
+
+
+
+ This should create a domain record at:
+
+
+ _atproto.{domain}
+
+ >
+ ) : (
+ <>
+
+ Upload a text file to:
+
+
+
+ https://{domain}/.well-known/atproto-did
+
+
+
+ That contains the following:
+
+
+ {currentAccount?.did}
+
+
+ >
+ )}
+
+ {isVerified && (
+
+
+
+ )}
+
+
+
+
+
+
+
+ )
+}
+
+class DidMismatchError extends Error {
+ did: string
+ constructor(did: string) {
+ super('DID mismatch')
+ this.name = 'DidMismatchError'
+ this.did = did
+ }
+}
+
+function ChangeHandleError({error}: {error: unknown}) {
+ const {_} = useLingui()
+
+ let message = _(msg`Failed to change handle. Please try again.`)
+
+ if (error instanceof Error) {
+ if (error.message.startsWith('Handle already taken')) {
+ message = _(msg`Handle already taken. Please try a different one.`)
+ } else if (error.message === 'Reserved handle') {
+ message = _(msg`This handle is reserved. Please try a different one.`)
+ } else if (error.message === 'Handle too long') {
+ message = _(msg`Handle too long. Please try a shorter one.`)
+ } else if (error.message === 'Input/handle must be a valid handle') {
+ message = _(msg`Invalid handle. Please try a different one.`)
+ } else if (error.message === 'Rate Limit Exceeded') {
+ message = _(
+ msg`Rate limit exceeded – you've tried to change your handle too many times in a short period. Please wait a minute before trying again.`,
+ )
+ }
+ }
+
+ return {message}
+}
+
+function SuccessMessage({text}: {text: string}) {
+ const {gtMobile} = useBreakpoints()
+ const t = useTheme()
+ return (
+
+
+
+
+ {text}
+
+ )
+}
diff --git a/src/screens/Settings/components/CopyButton.tsx b/src/screens/Settings/components/CopyButton.tsx
new file mode 100644
index 0000000000..eb538f5dee
--- /dev/null
+++ b/src/screens/Settings/components/CopyButton.tsx
@@ -0,0 +1,69 @@
+import React, {useCallback, useEffect, useState} from 'react'
+import {GestureResponderEvent, View} from 'react-native'
+import Animated, {FadeOutUp, ZoomIn} from 'react-native-reanimated'
+import * as Clipboard from 'expo-clipboard'
+import {Trans} from '@lingui/macro'
+
+import {atoms as a, useTheme} from '#/alf'
+import {Button, ButtonProps} from '#/components/Button'
+import {Text} from '#/components/Typography'
+
+export function CopyButton({
+ style,
+ value,
+ onPress: onPressProp,
+ ...props
+}: ButtonProps & {value: string}) {
+ const [hasBeenCopied, setHasBeenCopied] = useState(false)
+ const t = useTheme()
+
+ useEffect(() => {
+ if (hasBeenCopied) {
+ const timeout = setTimeout(() => setHasBeenCopied(false), 100)
+ return () => clearTimeout(timeout)
+ }
+ }, [hasBeenCopied])
+
+ const onPress = useCallback(
+ (evt: GestureResponderEvent) => {
+ Clipboard.setStringAsync(value)
+ setHasBeenCopied(true)
+ onPressProp?.(evt)
+ },
+ [value, onPressProp],
+ )
+
+ return (
+
+ {hasBeenCopied && (
+
+
+ Copied!
+
+
+ )}
+
+
+ )
+}
diff --git a/src/screens/Settings/components/SettingsList.tsx b/src/screens/Settings/components/SettingsList.tsx
index 86f8040afd..29ae9be6d9 100644
--- a/src/screens/Settings/components/SettingsList.tsx
+++ b/src/screens/Settings/components/SettingsList.tsx
@@ -2,7 +2,7 @@ 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 {atoms as a, useTheme, ViewStyleProp} 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'
@@ -17,7 +17,7 @@ const ItemContext = React.createContext({
const Portal = createPortalGroup()
export function Container({children}: {children: React.ReactNode}) {
- return {children}
+ return {children}
}
/**
@@ -241,11 +241,17 @@ export function ItemText({
}
}
-export function Divider() {
+export function Divider({style}: ViewStyleProp) {
const t = useTheme()
return (
)
}
diff --git a/src/state/queries/handle.ts b/src/state/queries/handle.ts
index d2d79e12d2..658b9e9c15 100644
--- a/src/state/queries/handle.ts
+++ b/src/state/queries/handle.ts
@@ -32,7 +32,9 @@ export function useFetchHandle() {
)
}
-export function useUpdateHandleMutation() {
+export function useUpdateHandleMutation(opts?: {
+ onSuccess?: (handle: string) => void
+}) {
const queryClient = useQueryClient()
const agent = useAgent()
@@ -41,6 +43,7 @@ export function useUpdateHandleMutation() {
await agent.updateHandle({handle})
},
onSuccess(_data, variables) {
+ opts?.onSuccess?.(variables.handle)
queryClient.invalidateQueries({
queryKey: fetchHandleQueryKey(variables.handle),
})
diff --git a/src/state/queries/notifications/settings.ts b/src/state/queries/notifications/settings.ts
index bfc449d17b..a17fce8326 100644
--- a/src/state/queries/notifications/settings.ts
+++ b/src/state/queries/notifications/settings.ts
@@ -9,7 +9,7 @@ import {invalidateCachedUnreadPage} from '#/state/queries/notifications/unread'
import {useAgent} from '#/state/session'
import * as Toast from '#/view/com/util/Toast'
-export function useNotificationsSettingsMutation() {
+export function useNotificationSettingsMutation() {
const {_} = useLingui()
const agent = useAgent()
const queryClient = useQueryClient()
diff --git a/src/view/screens/AppPasswords.tsx b/src/view/screens/AppPasswords.tsx
index 48abd964f9..09da3c1d2c 100644
--- a/src/view/screens/AppPasswords.tsx
+++ b/src/view/screens/AppPasswords.tsx
@@ -12,6 +12,7 @@ import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native'
import {NativeStackScreenProps} from '@react-navigation/native-stack'
+import {IS_INTERNAL} from '#/lib/app-info'
import {usePalette} from '#/lib/hooks/usePalette'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {CommonNavigatorParams} from '#/lib/routes/types'
@@ -28,14 +29,17 @@ import {Text} from '#/view/com/util/text/Text'
import * as Toast from '#/view/com/util/Toast'
import {ViewHeader} from '#/view/com/util/ViewHeader'
import {CenteredView} from '#/view/com/util/Views'
+import {AppPasswordsScreen as NewAppPasswordsScreen} from '#/screens/Settings/AppPasswords'
import {atoms as a} from '#/alf'
import {useDialogControl} from '#/components/Dialog'
import * as Layout from '#/components/Layout'
import * as Prompt from '#/components/Prompt'
type Props = NativeStackScreenProps
-export function AppPasswords({}: Props) {
- return (
+export function AppPasswords(props: Props) {
+ return IS_INTERNAL ? (
+
+ ) : (
diff --git a/src/view/screens/LanguageSettings.tsx b/src/view/screens/LanguageSettings.tsx
index 6af18103c1..f99cccee9d 100644
--- a/src/view/screens/LanguageSettings.tsx
+++ b/src/view/screens/LanguageSettings.tsx
@@ -10,6 +10,7 @@ import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native'
import {APP_LANGUAGES, LANGUAGES} from '#/lib/../locale/languages'
+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'
@@ -22,11 +23,20 @@ import {Button} from '#/view/com/util/forms/Button'
import {Text} from '#/view/com/util/text/Text'
import {ViewHeader} from '#/view/com/util/ViewHeader'
import {CenteredView} from '#/view/com/util/Views'
+import {LanguageSettingsScreen as NewLanguageSettingsScreen} from '#/screens/Settings/LanguageSettings'
import * as Layout from '#/components/Layout'
type Props = NativeStackScreenProps
-export function LanguageSettingsScreen(_props: Props) {
+export function LanguageSettingsScreen(props: Props) {
+ return IS_INTERNAL ? (
+
+ ) : (
+
+ )
+}
+
+function LegacyLanguageSettingsScreen(_props: Props) {
const pal = usePalette('default')
const {_} = useLingui()
const langPrefs = useLanguagePrefs()
diff --git a/src/view/screens/NotificationsSettings.tsx b/src/view/screens/NotificationsSettings.tsx
deleted file mode 100644
index f8d848a626..0000000000
--- a/src/view/screens/NotificationsSettings.tsx
+++ /dev/null
@@ -1,89 +0,0 @@
-import React from 'react'
-import {View} from 'react-native'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {msg, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-
-import {AllNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
-import {useNotificationFeedQuery} from '#/state/queries/notifications/feed'
-import {useNotificationsSettingsMutation} from '#/state/queries/notifications/settings'
-import {ViewHeader} from '#/view/com/util/ViewHeader'
-import {ScrollView} from '#/view/com/util/Views'
-import {atoms as a, useTheme} from '#/alf'
-import {Admonition} from '#/components/Admonition'
-import {Error} from '#/components/Error'
-import * as Toggle from '#/components/forms/Toggle'
-import * as Layout from '#/components/Layout'
-import {Loader} from '#/components/Loader'
-import {Text} from '#/components/Typography'
-
-type Props = NativeStackScreenProps
-export function NotificationsSettingsScreen({}: Props) {
- const {_} = useLingui()
- const t = useTheme()
-
- const {data, isError: isQueryError, refetch} = useNotificationFeedQuery()
- const serverPriority = data?.pages.at(0)?.priority
-
- const {
- mutate: onChangePriority,
- isPending: isMutationPending,
- variables,
- } = useNotificationsSettingsMutation()
-
- const priority = isMutationPending
- ? variables[0] === 'enabled'
- : serverPriority
-
- return (
-
-
-
- {isQueryError ? (
-
- ) : (
-
-
- {' '}
- Notification filters
-
-
-
-
-
- Enable priority notifications
-
- {!data ? : }
-
-
-
-
-
- Experimental: When this preference is enabled, you'll only
- receive reply and quote notifications from users you follow.
- We'll continue to add more controls here over time.
-
-
-
- )}
-
-
- )
-}
diff --git a/src/view/screens/PreferencesExternalEmbeds.tsx b/src/view/screens/PreferencesExternalEmbeds.tsx
index 5a657ce845..ef3f73b3cd 100644
--- a/src/view/screens/PreferencesExternalEmbeds.tsx
+++ b/src/view/screens/PreferencesExternalEmbeds.tsx
@@ -3,6 +3,7 @@ import {StyleSheet, View} from 'react-native'
import {Trans} from '@lingui/macro'
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'
@@ -19,6 +20,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 {ExternalMediaPreferencesScreen} from '#/screens/Settings/ExternalMediaPreferences'
import {atoms as a} from '#/alf'
import * as Layout from '#/components/Layout'
@@ -26,7 +28,15 @@ type Props = NativeStackScreenProps<
CommonNavigatorParams,
'PreferencesExternalEmbeds'
>
-export function PreferencesExternalEmbeds({}: Props) {
+export function PreferencesExternalEmbeds(props: Props) {
+ return IS_INTERNAL ? (
+
+ ) : (
+
+ )
+}
+
+function LegacyPreferencesExternalEmbeds({}: Props) {
const pal = usePalette('default')
const setMinimalShellMode = useSetMinimalShellMode()
const {isTabletOrMobile} = useWebMediaQueries()
diff --git a/src/view/screens/PreferencesFollowingFeed.tsx b/src/view/screens/PreferencesFollowingFeed.tsx
index 3d9928901a..c31a23c49a 100644
--- a/src/view/screens/PreferencesFollowingFeed.tsx
+++ b/src/view/screens/PreferencesFollowingFeed.tsx
@@ -4,6 +4,7 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
+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'
@@ -16,6 +17,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 {FollowingFeedPreferencesScreen} from '#/screens/Settings/FollowingFeedPreferences'
import {atoms as a} from '#/alf'
import * as Layout from '#/components/Layout'
@@ -23,7 +25,15 @@ type Props = NativeStackScreenProps<
CommonNavigatorParams,
'PreferencesFollowingFeed'
>
-export function PreferencesFollowingFeed({}: Props) {
+export function PreferencesFollowingFeed(props: Props) {
+ return IS_INTERNAL ? (
+
+ ) : (
+
+ )
+}
+
+function LegacyPreferencesFollowingFeed({}: Props) {
const pal = usePalette('default')
const {_} = useLingui()
const {isTabletOrMobile} = useWebMediaQueries()
diff --git a/src/view/screens/PreferencesThreads.tsx b/src/view/screens/PreferencesThreads.tsx
index c3992276a9..f511f4c59e 100644
--- a/src/view/screens/PreferencesThreads.tsx
+++ b/src/view/screens/PreferencesThreads.tsx
@@ -4,6 +4,7 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
+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'
@@ -17,11 +18,20 @@ 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 {ThreadPreferencesScreen} from '#/screens/Settings/ThreadPreferences'
import {atoms as a} from '#/alf'
import * as Layout from '#/components/Layout'
type Props = NativeStackScreenProps
-export function PreferencesThreads({}: Props) {
+export function PreferencesThreads(props: Props) {
+ return IS_INTERNAL ? (
+
+ ) : (
+
+ )
+}
+
+function LegacyPreferencesThreads({}: Props) {
const pal = usePalette('default')
const {_} = useLingui()
const {isTabletOrMobile} = useWebMediaQueries()