about summary refs log tree commit diff
path: root/src/view
diff options
context:
space:
mode:
Diffstat (limited to 'src/view')
-rw-r--r--src/view/com/auth/HomeLoggedOutCTA.tsx21
-rw-r--r--src/view/com/auth/LoggedOut.tsx41
-rw-r--r--src/view/com/auth/SplashScreen.tsx174
-rw-r--r--src/view/com/auth/SplashScreen.web.tsx280
-rw-r--r--src/view/com/auth/create/CreateAccount.tsx219
-rw-r--r--src/view/com/auth/create/Policies.tsx114
-rw-r--r--src/view/com/auth/create/Step1.tsx283
-rw-r--r--src/view/com/auth/create/Step2.tsx307
-rw-r--r--src/view/com/auth/create/Step3.tsx62
-rw-r--r--src/view/com/auth/create/StepHeader.tsx44
-rw-r--r--src/view/com/auth/create/state.ts350
-rw-r--r--src/view/com/auth/login/ChooseAccountForm.tsx158
-rw-r--r--src/view/com/auth/login/ForgotPasswordForm.tsx228
-rw-r--r--src/view/com/auth/login/Login.tsx164
-rw-r--r--src/view/com/auth/login/LoginForm.tsx298
-rw-r--r--src/view/com/auth/login/PasswordUpdatedForm.tsx48
-rw-r--r--src/view/com/auth/login/SetNewPasswordForm.tsx211
-rw-r--r--src/view/com/auth/login/styles.ts118
-rw-r--r--src/view/com/auth/onboarding/RecommendedFollowsItem.tsx23
-rw-r--r--src/view/com/auth/onboarding/WelcomeMobile.tsx6
-rw-r--r--src/view/com/auth/server-input/index.tsx8
-rw-r--r--src/view/com/composer/Composer.tsx160
-rw-r--r--src/view/com/composer/ComposerReplyTo.tsx7
-rw-r--r--src/view/com/composer/Prompt.tsx6
-rw-r--r--src/view/com/composer/photos/OpenCameraBtn.tsx19
-rw-r--r--src/view/com/composer/select-language/SelectLangBtn.tsx8
-rw-r--r--src/view/com/composer/text-input/TextInput.tsx3
-rw-r--r--src/view/com/composer/text-input/TextInput.web.tsx2
-rw-r--r--src/view/com/composer/text-input/mobile/Autocomplete.tsx6
-rw-r--r--src/view/com/composer/text-input/web/Autocomplete.tsx6
-rw-r--r--src/view/com/composer/text-input/web/LinkDecorator.ts5
-rw-r--r--src/view/com/composer/text-input/web/TagDecorator.ts91
-rw-r--r--src/view/com/composer/useExternalLinkFetch.e2e.ts45
-rw-r--r--src/view/com/feeds/FeedPage.tsx109
-rw-r--r--src/view/com/feeds/FeedSourceCard.tsx263
-rw-r--r--src/view/com/home/HomeHeader.tsx67
-rw-r--r--src/view/com/home/HomeHeaderLayout.tsx1
-rw-r--r--src/view/com/home/HomeHeaderLayout.web.tsx111
-rw-r--r--src/view/com/home/HomeHeaderLayoutMobile.tsx (renamed from src/view/com/pager/FeedsTabBarMobile.tsx)90
-rw-r--r--src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx60
-rw-r--r--src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx1
-rw-r--r--src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx11
-rw-r--r--src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.tsx1
-rw-r--r--src/view/com/lightbox/ImageViewing/index.tsx1
-rw-r--r--src/view/com/lightbox/Lightbox.tsx4
-rw-r--r--src/view/com/lightbox/Lightbox.web.tsx54
-rw-r--r--src/view/com/lists/ListCard.tsx7
-rw-r--r--src/view/com/modals/AppealLabel.tsx139
-rw-r--r--src/view/com/modals/BirthDateSettings.tsx151
-rw-r--r--src/view/com/modals/ChangeHandle.tsx46
-rw-r--r--src/view/com/modals/ChangePassword.tsx8
-rw-r--r--src/view/com/modals/Confirm.tsx132
-rw-r--r--src/view/com/modals/ContentFilteringSettings.tsx401
-rw-r--r--src/view/com/modals/DeleteAccount.tsx35
-rw-r--r--src/view/com/modals/InAppBrowserConsent.tsx2
-rw-r--r--src/view/com/modals/LinkWarning.tsx8
-rw-r--r--src/view/com/modals/ListAddRemoveUsers.tsx6
-rw-r--r--src/view/com/modals/Modal.tsx70
-rw-r--r--src/view/com/modals/Modal.web.tsx23
-rw-r--r--src/view/com/modals/ModerationDetails.tsx142
-rw-r--r--src/view/com/modals/SwitchAccount.tsx37
-rw-r--r--src/view/com/modals/UserAddRemoveLists.tsx2
-rw-r--r--src/view/com/modals/VerifyEmail.tsx2
-rw-r--r--src/view/com/modals/Waitlist.tsx190
-rw-r--r--src/view/com/modals/crop-image/CropImage.web.tsx12
-rw-r--r--src/view/com/modals/report/InputIssueDetails.tsx100
-rw-r--r--src/view/com/modals/report/Modal.tsx223
-rw-r--r--src/view/com/modals/report/ReasonOptions.tsx123
-rw-r--r--src/view/com/modals/report/SendReportButton.tsx62
-rw-r--r--src/view/com/modals/report/types.ts8
-rw-r--r--src/view/com/modals/util.tsx2
-rw-r--r--src/view/com/notifications/FeedItem.tsx19
-rw-r--r--src/view/com/pager/FeedsTabBar.tsx1
-rw-r--r--src/view/com/pager/FeedsTabBar.web.tsx155
-rw-r--r--src/view/com/pager/FixedTouchableHighlight.tsx42
-rw-r--r--src/view/com/pager/PagerWithHeader.tsx35
-rw-r--r--src/view/com/pager/TabBar.tsx119
-rw-r--r--src/view/com/post-thread/PostLikedBy.tsx4
-rw-r--r--src/view/com/post-thread/PostThread.tsx675
-rw-r--r--src/view/com/post-thread/PostThreadFollowBtn.tsx5
-rw-r--r--src/view/com/post-thread/PostThreadItem.tsx160
-rw-r--r--src/view/com/post/Post.tsx53
-rw-r--r--src/view/com/posts/Feed.tsx26
-rw-r--r--src/view/com/posts/FeedErrorMessage.tsx97
-rw-r--r--src/view/com/posts/FeedItem.tsx74
-rw-r--r--src/view/com/posts/FeedSlice.tsx6
-rw-r--r--src/view/com/profile/FollowButton.tsx7
-rw-r--r--src/view/com/profile/ProfileCard.tsx100
-rw-r--r--src/view/com/profile/ProfileFollowers.tsx124
-rw-r--r--src/view/com/profile/ProfileFollows.tsx123
-rw-r--r--src/view/com/profile/ProfileHeader.tsx784
-rw-r--r--src/view/com/profile/ProfileHeaderSuggestedFollows.tsx31
-rw-r--r--src/view/com/profile/ProfileMenu.tsx380
-rw-r--r--src/view/com/testing/TestCtrls.e2e.tsx28
-rw-r--r--src/view/com/util/BottomSheetCustomBackdrop.tsx9
-rw-r--r--src/view/com/util/ErrorBoundary.tsx23
-rw-r--r--src/view/com/util/EventStopper.tsx17
-rw-r--r--src/view/com/util/Link.tsx67
-rw-r--r--src/view/com/util/List.web.tsx4
-rw-r--r--src/view/com/util/MainScrollProvider.tsx16
-rw-r--r--src/view/com/util/PostMeta.tsx18
-rw-r--r--src/view/com/util/PostSandboxWarning.tsx35
-rw-r--r--src/view/com/util/UserAvatar.tsx266
-rw-r--r--src/view/com/util/UserBanner.tsx243
-rw-r--r--src/view/com/util/ViewHeader.tsx97
-rw-r--r--src/view/com/util/Views.d.ts4
-rw-r--r--src/view/com/util/Views.web.tsx11
-rw-r--r--src/view/com/util/forms/DateInput.tsx32
-rw-r--r--src/view/com/util/forms/NativeDropdown.tsx3
-rw-r--r--src/view/com/util/forms/NativeDropdown.web.tsx12
-rw-r--r--src/view/com/util/forms/PostDropdownBtn.tsx421
-rw-r--r--src/view/com/util/forms/SelectableBtn.tsx1
-rw-r--r--src/view/com/util/layouts/LoggedOutLayout.tsx55
-rw-r--r--src/view/com/util/load-latest/LoadLatestBtn.tsx17
-rw-r--r--src/view/com/util/moderation/ContentHider.tsx145
-rw-r--r--src/view/com/util/moderation/LabelInfo.tsx61
-rw-r--r--src/view/com/util/moderation/PostAlerts.tsx67
-rw-r--r--src/view/com/util/moderation/PostHider.tsx142
-rw-r--r--src/view/com/util/moderation/ProfileHeaderAlerts.tsx89
-rw-r--r--src/view/com/util/moderation/ScreenHider.tsx180
-rw-r--r--src/view/com/util/post-ctrls/PostCtrls.tsx101
-rw-r--r--src/view/com/util/post-embeds/ExternalPlayerEmbed.tsx94
-rw-r--r--src/view/com/util/post-embeds/QuoteEmbed.tsx131
-rw-r--r--src/view/com/util/post-embeds/index.tsx106
-rw-r--r--src/view/com/util/text/RichText.tsx70
-rw-r--r--src/view/com/util/text/Text.tsx15
-rw-r--r--src/view/icons/index.tsx2
-rw-r--r--src/view/screens/AppPasswords.tsx36
-rw-r--r--src/view/screens/DebugMod.tsx923
-rw-r--r--src/view/screens/Feeds.tsx215
-rw-r--r--src/view/screens/Home.tsx15
-rw-r--r--src/view/screens/LanguageSettings.tsx4
-rw-r--r--src/view/screens/Moderation.tsx285
-rw-r--r--src/view/screens/ModerationBlockedAccounts.tsx2
-rw-r--r--src/view/screens/ModerationMutedAccounts.tsx4
-rw-r--r--src/view/screens/NotFound.tsx8
-rw-r--r--src/view/screens/PostThread.tsx6
-rw-r--r--src/view/screens/PreferencesFollowingFeed.tsx (renamed from src/view/screens/PreferencesHomeFeed.tsx)10
-rw-r--r--src/view/screens/Profile.tsx301
-rw-r--r--src/view/screens/ProfileFeed.tsx392
-rw-r--r--src/view/screens/ProfileFollowers.tsx2
-rw-r--r--src/view/screens/ProfileFollows.tsx2
-rw-r--r--src/view/screens/ProfileList.tsx239
-rw-r--r--src/view/screens/Search/Search.tsx116
-rw-r--r--src/view/screens/Settings/ExportCarDialog.tsx5
-rw-r--r--src/view/screens/Settings/index.tsx182
-rw-r--r--src/view/screens/Storybook/Buttons.tsx41
-rw-r--r--src/view/screens/Storybook/Dialogs.tsx51
-rw-r--r--src/view/screens/Storybook/Icons.tsx9
-rw-r--r--src/view/screens/Storybook/Links.tsx25
-rw-r--r--src/view/screens/Storybook/Menus.tsx79
-rw-r--r--src/view/screens/Storybook/Palette.tsx182
-rw-r--r--src/view/screens/Storybook/Typography.tsx11
-rw-r--r--src/view/screens/Storybook/index.tsx3
-rw-r--r--src/view/shell/Composer.tsx2
-rw-r--r--src/view/shell/Composer.web.tsx3
-rw-r--r--src/view/shell/Drawer.tsx11
-rw-r--r--src/view/shell/NavSignupCard.tsx2
-rw-r--r--src/view/shell/bottom-bar/BottomBar.tsx10
-rw-r--r--src/view/shell/desktop/Feeds.tsx6
-rw-r--r--src/view/shell/desktop/LeftNav.tsx14
-rw-r--r--src/view/shell/desktop/RightNav.tsx12
-rw-r--r--src/view/shell/desktop/Search.tsx9
-rw-r--r--src/view/shell/index.tsx13
-rw-r--r--src/view/shell/index.web.tsx16
165 files changed, 5544 insertions, 9523 deletions
diff --git a/src/view/com/auth/HomeLoggedOutCTA.tsx b/src/view/com/auth/HomeLoggedOutCTA.tsx
index f796d8bae..4c8c35da7 100644
--- a/src/view/com/auth/HomeLoggedOutCTA.tsx
+++ b/src/view/com/auth/HomeLoggedOutCTA.tsx
@@ -1,14 +1,15 @@
 import React from 'react'
 import {StyleSheet, TouchableOpacity, View} from 'react-native'
+import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
-import {Trans, msg} from '@lingui/macro'
-import {ScrollView} from '../util/Views'
-import {Text} from '../util/text/Text'
+
 import {usePalette} from '#/lib/hooks/usePalette'
-import {colors, s} from '#/lib/styles'
-import {TextLink} from '../util/Link'
 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
+import {colors, s} from '#/lib/styles'
 import {useLoggedOutViewControls} from '#/state/shell/logged-out'
+import {TextLink} from '../util/Link'
+import {Text} from '../util/text/Text'
+import {ScrollView} from '../util/Views'
 
 export function HomeLoggedOutCTA() {
   const pal = usePalette('default')
@@ -52,7 +53,9 @@ export function HomeLoggedOutCTA() {
           onPress={showCreateAccount}
           accessibilityRole="button"
           accessibilityLabel={_(msg`Create new account`)}
-          accessibilityHint="Opens flow to create a new Bluesky account">
+          accessibilityHint={_(
+            msg`Opens flow to create a new Bluesky account`,
+          )}>
           <Text
             style={[
               s.white,
@@ -68,14 +71,16 @@ export function HomeLoggedOutCTA() {
           onPress={showSignIn}
           accessibilityRole="button"
           accessibilityLabel={_(msg`Sign in`)}
-          accessibilityHint="Opens flow to sign into your existing Bluesky account">
+          accessibilityHint={_(
+            msg`Opens flow to sign into your existing Bluesky account`,
+          )}>
           <Text
             style={[
               pal.text,
               styles.btnLabel,
               isMobile && styles.btnLabelMobile,
             ]}>
-            <Trans>Sign In</Trans>
+            <Trans>Sign in</Trans>
           </Text>
         </TouchableOpacity>
       </View>
diff --git a/src/view/com/auth/LoggedOut.tsx b/src/view/com/auth/LoggedOut.tsx
index 603abbab2..c8c81dd77 100644
--- a/src/view/com/auth/LoggedOut.tsx
+++ b/src/view/com/auth/LoggedOut.tsx
@@ -1,27 +1,28 @@
 import React from 'react'
-import {View, Pressable} from 'react-native'
+import {Pressable, View} from 'react-native'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
-import {Trans, msg} from '@lingui/macro'
 import {useNavigation} from '@react-navigation/native'
 
-import {isIOS, isNative} from 'platform/detection'
-import {Login} from 'view/com/auth/login/Login'
-import {CreateAccount} from 'view/com/auth/create/CreateAccount'
-import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
-import {s} from 'lib/styles'
-import {usePalette} from 'lib/hooks/usePalette'
-import {useAnalytics} from 'lib/analytics/analytics'
-import {SplashScreen} from './SplashScreen'
-import {useSetMinimalShellMode} from '#/state/shell/minimal-mode'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
+import {useAnalytics} from '#/lib/analytics/analytics'
+import {usePalette} from '#/lib/hooks/usePalette'
+import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
+import {logEvent} from '#/lib/statsig/statsig'
+import {s} from '#/lib/styles'
+import {isIOS, isNative} from '#/platform/detection'
+import {useSession} from '#/state/session'
 import {
   useLoggedOutView,
   useLoggedOutViewControls,
 } from '#/state/shell/logged-out'
-import {useSession} from '#/state/session'
-import {Text} from '#/view/com/util/text/Text'
+import {useSetMinimalShellMode} from '#/state/shell/minimal-mode'
 import {NavigationProp} from 'lib/routes/types'
+import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
+import {Text} from '#/view/com/util/text/Text'
+import {Login} from '#/screens/Login'
+import {Signup} from '#/screens/Signup'
+import {SplashScreen} from './SplashScreen'
 
 enum ScreenState {
   S_LoginOrCreateAccount,
@@ -133,10 +134,14 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
 
         {screenState === ScreenState.S_LoginOrCreateAccount ? (
           <SplashScreen
-            onPressSignin={() => setScreenState(ScreenState.S_Login)}
-            onPressCreateAccount={() =>
+            onPressSignin={() => {
+              setScreenState(ScreenState.S_Login)
+              logEvent('splash:signInPressed', {})
+            }}
+            onPressCreateAccount={() => {
               setScreenState(ScreenState.S_CreateAccount)
-            }
+              logEvent('splash:createAccountPressed', {})
+            }}
           />
         ) : undefined}
         {screenState === ScreenState.S_Login ? (
@@ -148,7 +153,7 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
           />
         ) : undefined}
         {screenState === ScreenState.S_CreateAccount ? (
-          <CreateAccount
+          <Signup
             onPressBack={() =>
               setScreenState(ScreenState.S_LoginOrCreateAccount)
             }
diff --git a/src/view/com/auth/SplashScreen.tsx b/src/view/com/auth/SplashScreen.tsx
index ffd07d945..763b01dfa 100644
--- a/src/view/com/auth/SplashScreen.tsx
+++ b/src/view/com/auth/SplashScreen.tsx
@@ -1,14 +1,21 @@
 import React from 'react'
-import {StyleSheet, TouchableOpacity, View} from 'react-native'
-import {Text} from 'view/com/util/text/Text'
-import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
-import {s, colors} from 'lib/styles'
-import {usePalette} from 'lib/hooks/usePalette'
-import {CenteredView} from '../util/Views'
-import {Trans, msg} from '@lingui/macro'
+import {View} from 'react-native'
+import RNPickerSelect, {PickerSelectProps} from 'react-native-picker-select'
+import {useSafeAreaInsets} from 'react-native-safe-area-context'
+import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
+
+import {sanitizeAppLanguageSetting} from '#/locale/helpers'
+import {APP_LANGUAGES} from '#/locale/languages'
+import {useLanguagePrefs, useLanguagePrefsApi} from '#/state/preferences'
 import {Logo} from '#/view/icons/Logo'
 import {Logotype} from '#/view/icons/Logotype'
+import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
+import {atoms as a, useTheme} from '#/alf'
+import {Button, ButtonText} from '#/components/Button'
+import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron'
+import {Text} from '#/components/Typography'
+import {CenteredView} from '../util/Views'
 
 export const SplashScreen = ({
   onPressSignin,
@@ -17,82 +24,121 @@ export const SplashScreen = ({
   onPressSignin: () => void
   onPressCreateAccount: () => void
 }) => {
-  const pal = usePalette('default')
+  const t = useTheme()
   const {_} = useLingui()
 
+  const langPrefs = useLanguagePrefs()
+  const setLangPrefs = useLanguagePrefsApi()
+  const insets = useSafeAreaInsets()
+
+  const sanitizedLang = sanitizeAppLanguageSetting(langPrefs.appLanguage)
+
+  const onChangeAppLanguage = React.useCallback(
+    (value: Parameters<PickerSelectProps['onValueChange']>[0]) => {
+      if (!value) return
+      if (sanitizedLang !== value) {
+        setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value))
+      }
+    },
+    [sanitizedLang, setLangPrefs],
+  )
+
   return (
-    <CenteredView style={[styles.container, pal.view]}>
+    <CenteredView style={[a.h_full, a.flex_1]}>
       <ErrorBoundary>
-        <View style={styles.hero}>
+        <View style={[{flex: 1}, a.justify_center, a.align_center]}>
           <Logo width={92} fill="sky" />
 
-          <View style={{paddingTop: 40, paddingBottom: 6}}>
-            <Logotype width={161} fill={pal.text.color} />
+          <View style={[a.pb_sm, a.pt_5xl]}>
+            <Logotype width={161} fill={t.atoms.text.color} />
           </View>
 
-          <Text type="lg-medium" style={[pal.textLight]}>
+          <Text
+            style={[a.text_md, a.font_semibold, t.atoms.text_contrast_medium]}>
             <Trans>What's up?</Trans>
           </Text>
         </View>
-        <View testID="signinOrCreateAccount" style={styles.btns}>
-          <TouchableOpacity
+        <View testID="signinOrCreateAccount">
+          <Button
             testID="createAccountButton"
-            style={[styles.btn, {backgroundColor: colors.blue3}]}
             onPress={onPressCreateAccount}
             accessibilityRole="button"
-            accessibilityLabel={_(msg`Create new account`)}
-            accessibilityHint="Opens flow to create a new Bluesky account">
-            <Text style={[s.white, styles.btnLabel]}>
+            label={_(msg`Create new account`)}
+            accessibilityHint={_(
+              msg`Opens flow to create a new Bluesky account`,
+            )}
+            style={[a.mx_xl, a.mb_xl]}
+            size="large"
+            variant="solid"
+            color="primary">
+            <ButtonText>
               <Trans>Create a new account</Trans>
-            </Text>
-          </TouchableOpacity>
-          <TouchableOpacity
+            </ButtonText>
+          </Button>
+          <Button
             testID="signInButton"
-            style={[styles.btn, pal.btn]}
             onPress={onPressSignin}
-            accessibilityRole="button"
-            accessibilityLabel={_(msg`Sign in`)}
-            accessibilityHint="Opens flow to sign into your existing Bluesky account">
-            <Text style={[pal.text, styles.btnLabel]}>
-              <Trans>Sign In</Trans>
-            </Text>
-          </TouchableOpacity>
+            label={_(msg`Sign in`)}
+            accessibilityHint={_(
+              msg`Opens flow to sign into your existing Bluesky account`,
+            )}
+            style={[a.mx_xl, a.mb_xl]}
+            size="large"
+            variant="solid"
+            color="secondary">
+            <ButtonText>
+              <Trans>Sign in</Trans>
+            </ButtonText>
+          </Button>
         </View>
+        <View
+          style={[
+            a.px_lg,
+            a.pt_md,
+            a.pb_2xl,
+            a.justify_center,
+            a.align_center,
+          ]}>
+          <View style={a.relative}>
+            <RNPickerSelect
+              placeholder={{}}
+              value={sanitizedLang}
+              onValueChange={onChangeAppLanguage}
+              items={APP_LANGUAGES.filter(l => Boolean(l.code2)).map(l => ({
+                label: l.name,
+                value: l.code2,
+                key: l.code2,
+              }))}
+              useNativeAndroidPickerStyle={false}
+              style={{
+                inputAndroid: {
+                  color: t.atoms.text_contrast_medium.color,
+                  fontSize: 16,
+                  paddingRight: 12 + 4,
+                },
+                inputIOS: {
+                  color: t.atoms.text.color,
+                  fontSize: 16,
+                  paddingRight: 12 + 4,
+                },
+              }}
+            />
+
+            <View
+              style={[
+                a.absolute,
+                a.inset_0,
+                {left: 'auto'},
+                {pointerEvents: 'none'},
+                a.align_center,
+                a.justify_center,
+              ]}>
+              <ChevronDown fill={t.atoms.text.color} size="xs" />
+            </View>
+          </View>
+        </View>
+        <View style={{height: insets.bottom}} />
       </ErrorBoundary>
     </CenteredView>
   )
 }
-
-const styles = StyleSheet.create({
-  container: {
-    height: '100%',
-  },
-  hero: {
-    flex: 2,
-    justifyContent: 'center',
-    alignItems: 'center',
-  },
-  btns: {
-    paddingBottom: 40,
-  },
-  title: {
-    textAlign: 'center',
-    fontSize: 68,
-    fontWeight: 'bold',
-  },
-  subtitle: {
-    textAlign: 'center',
-    fontSize: 42,
-    fontWeight: 'bold',
-  },
-  btn: {
-    borderRadius: 32,
-    paddingVertical: 16,
-    marginBottom: 20,
-    marginHorizontal: 20,
-  },
-  btnLabel: {
-    textAlign: 'center',
-    fontSize: 21,
-  },
-})
diff --git a/src/view/com/auth/SplashScreen.web.tsx b/src/view/com/auth/SplashScreen.web.tsx
index 8ef64099f..cdb72cc04 100644
--- a/src/view/com/auth/SplashScreen.web.tsx
+++ b/src/view/com/auth/SplashScreen.web.tsx
@@ -1,17 +1,22 @@
 import React from 'react'
-import {StyleSheet, TouchableOpacity, View, Pressable} from 'react-native'
+import {Pressable, View} from 'react-native'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {Text} from 'view/com/util/text/Text'
-import {TextLink} from '../util/Link'
-import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
-import {s, colors} from 'lib/styles'
-import {usePalette} from 'lib/hooks/usePalette'
-import {CenteredView} from '../util/Views'
-import {isWeb} from 'platform/detection'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {sanitizeAppLanguageSetting} from '#/locale/helpers'
+import {APP_LANGUAGES} from '#/locale/languages'
+import {useLanguagePrefs, useLanguagePrefsApi} from '#/state/preferences'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {Trans} from '@lingui/macro'
 import {Logo} from '#/view/icons/Logo'
 import {Logotype} from '#/view/icons/Logotype'
+import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
+import {atoms as a, useTheme} from '#/alf'
+import {Button, ButtonText} from '#/components/Button'
+import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron'
+import {InlineLink} from '#/components/Link'
+import {Text} from '#/components/Typography'
+import {CenteredView} from '../util/Views'
 
 export const SplashScreen = ({
   onDismiss,
@@ -22,10 +27,9 @@ export const SplashScreen = ({
   onPressSignin: () => void
   onPressCreateAccount: () => void
 }) => {
-  const pal = usePalette('default')
-  const {isTabletOrMobile} = useWebMediaQueries()
-  const styles = useStyles()
-  const isMobileWeb = isWeb && isTabletOrMobile
+  const {_} = useLingui()
+  const t = useTheme()
+  const {isTabletOrMobile: isMobileWeb} = useWebMediaQueries()
 
   return (
     <>
@@ -44,153 +48,165 @@ export const SplashScreen = ({
             icon="x"
             size={24}
             style={{
-              color: String(pal.text.color),
+              color: String(t.atoms.text.color),
             }}
           />
         </Pressable>
       )}
 
-      <CenteredView style={[styles.container, pal.view]}>
+      <CenteredView style={[a.h_full, a.flex_1]}>
         <View
           testID="noSessionView"
           style={[
-            styles.containerInner,
-            isMobileWeb && styles.containerInnerMobile,
-            pal.border,
-            {alignItems: 'center'},
+            a.h_full,
+            a.justify_center,
+            // @ts-ignore web only
+            {paddingBottom: '20vh'},
+            isMobileWeb && a.pb_5xl,
+            t.atoms.border_contrast_medium,
+            a.align_center,
+            a.gap_5xl,
           ]}>
           <ErrorBoundary>
-            <Logo width={92} fill="sky" />
+            <View style={[a.justify_center, a.align_center]}>
+              <Logo width={92} fill="sky" />
+
+              <View style={[a.pb_sm, a.pt_5xl]}>
+                <Logotype width={161} fill={t.atoms.text.color} />
+              </View>
 
-            <View style={{paddingTop: 40, paddingBottom: 20}}>
-              <Logotype width={161} fill={pal.text.color} />
+              <Text
+                style={[
+                  a.text_md,
+                  a.font_semibold,
+                  t.atoms.text_contrast_medium,
+                ]}>
+                <Trans>What's up?</Trans>
+              </Text>
             </View>
 
-            <View testID="signinOrCreateAccount" style={styles.btns}>
-              <TouchableOpacity
+            <View
+              testID="signinOrCreateAccount"
+              style={[a.w_full, {maxWidth: 320}]}>
+              <Button
                 testID="createAccountButton"
-                style={[styles.btn, {backgroundColor: colors.blue3}]}
                 onPress={onPressCreateAccount}
-                // TODO: web accessibility
-                accessibilityRole="button">
-                <Text style={[s.white, styles.btnLabel]}>
+                accessibilityRole="button"
+                label={_(msg`Create new account`)}
+                accessibilityHint={_(
+                  msg`Opens flow to create a new Bluesky account`,
+                )}
+                style={[a.mx_xl, a.mb_xl]}
+                size="large"
+                variant="solid"
+                color="primary">
+                <ButtonText>
                   <Trans>Create a new account</Trans>
-                </Text>
-              </TouchableOpacity>
-              <TouchableOpacity
+                </ButtonText>
+              </Button>
+              <Button
                 testID="signInButton"
-                style={[styles.btn, pal.btn]}
                 onPress={onPressSignin}
-                // TODO: web accessibility
-                accessibilityRole="button">
-                <Text style={[pal.text, styles.btnLabel]}>
-                  <Trans>Sign In</Trans>
-                </Text>
-              </TouchableOpacity>
+                label={_(msg`Sign in`)}
+                accessibilityHint={_(
+                  msg`Opens flow to sign into your existing Bluesky account`,
+                )}
+                style={[a.mx_xl, a.mb_xl]}
+                size="large"
+                variant="solid"
+                color="secondary">
+                <ButtonText>
+                  <Trans>Sign in</Trans>
+                </ButtonText>
+              </Button>
             </View>
           </ErrorBoundary>
         </View>
-        <Footer styles={styles} />
+        <Footer />
       </CenteredView>
     </>
   )
 }
 
-function Footer({styles}: {styles: ReturnType<typeof useStyles>}) {
-  const pal = usePalette('default')
+function Footer() {
+  const t = useTheme()
+
+  const langPrefs = useLanguagePrefs()
+  const setLangPrefs = useLanguagePrefsApi()
+
+  const sanitizedLang = sanitizeAppLanguageSetting(langPrefs.appLanguage)
+
+  const onChangeAppLanguage = React.useCallback(
+    (ev: React.ChangeEvent<HTMLSelectElement>) => {
+      const value = ev.target.value
+
+      if (!value) return
+      if (sanitizedLang !== value) {
+        setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value))
+      }
+    },
+    [sanitizedLang, setLangPrefs],
+  )
 
   return (
-    <View style={[styles.footer, pal.view, pal.border]}>
-      <TextLink
-        href="https://bsky.social"
-        text="Business"
-        style={[styles.footerLink, pal.link]}
-      />
-      <TextLink
-        href="https://bsky.social/about/blog"
-        text="Blog"
-        style={[styles.footerLink, pal.link]}
-      />
-      <TextLink
-        href="https://bsky.social/about/join"
-        text="Jobs"
-        style={[styles.footerLink, pal.link]}
-      />
+    <View
+      style={[
+        a.absolute,
+        a.inset_0,
+        {top: 'auto'},
+        a.p_xl,
+        a.border_t,
+        a.flex_row,
+        a.flex_wrap,
+        a.gap_xl,
+        a.flex_1,
+        t.atoms.border_contrast_medium,
+      ]}>
+      <InlineLink to="https://bsky.social">
+        <Trans>Business</Trans>
+      </InlineLink>
+      <InlineLink to="https://bsky.social/about/blog">
+        <Trans>Blog</Trans>
+      </InlineLink>
+      <InlineLink to="https://bsky.social/about/join">
+        <Trans>Jobs</Trans>
+      </InlineLink>
+
+      <View style={a.flex_1} />
+
+      <View style={[a.flex_row, a.gap_sm, a.align_center, a.flex_shrink]}>
+        <Text aria-hidden={true} style={t.atoms.text_contrast_medium}>
+          {APP_LANGUAGES.find(l => l.code2 === sanitizedLang)?.name}
+        </Text>
+        <ChevronDown
+          fill={t.atoms.text.color}
+          size="xs"
+          style={a.flex_shrink}
+        />
+
+        <select
+          value={sanitizedLang}
+          onChange={onChangeAppLanguage}
+          style={{
+            cursor: 'pointer',
+            MozAppearance: 'none',
+            WebkitAppearance: 'none',
+            appearance: 'none',
+            position: 'absolute',
+            inset: 0,
+            width: '100%',
+            color: 'transparent',
+            background: 'transparent',
+            border: 0,
+            padding: 0,
+          }}>
+          {APP_LANGUAGES.filter(l => Boolean(l.code2)).map(l => (
+            <option key={l.code2} value={l.code2}>
+              {l.name}
+            </option>
+          ))}
+        </select>
+      </View>
     </View>
   )
 }
-const useStyles = () => {
-  return StyleSheet.create({
-    container: {
-      height: '100%',
-    },
-    containerInner: {
-      height: '100%',
-      justifyContent: 'center',
-      // @ts-ignore web only
-      paddingBottom: '20vh',
-      paddingHorizontal: 20,
-    },
-    containerInnerMobile: {
-      paddingBottom: 50,
-    },
-    title: {
-      textAlign: 'center',
-      color: colors.blue3,
-      fontSize: 68,
-      fontWeight: 'bold',
-      paddingBottom: 10,
-    },
-    titleMobile: {
-      textAlign: 'center',
-      color: colors.blue3,
-      fontSize: 58,
-      fontWeight: 'bold',
-    },
-    subtitle: {
-      textAlign: 'center',
-      color: colors.gray5,
-      fontSize: 52,
-      fontWeight: 'bold',
-      paddingBottom: 30,
-    },
-    subtitleMobile: {
-      textAlign: 'center',
-      color: colors.gray5,
-      fontSize: 42,
-      fontWeight: 'bold',
-      paddingBottom: 30,
-    },
-    btns: {
-      gap: 10,
-      justifyContent: 'center',
-      paddingBottom: 40,
-    },
-    btn: {
-      borderRadius: 30,
-      paddingHorizontal: 24,
-      paddingVertical: 12,
-      minWidth: 220,
-    },
-    btnLabel: {
-      textAlign: 'center',
-      fontSize: 18,
-    },
-    notice: {
-      paddingHorizontal: 40,
-      textAlign: 'center',
-    },
-    footer: {
-      position: 'absolute',
-      left: 0,
-      right: 0,
-      bottom: 0,
-      padding: 20,
-      borderTopWidth: 1,
-      flexDirection: 'row',
-    },
-    footerLink: {
-      marginRight: 20,
-    },
-  })
-}
diff --git a/src/view/com/auth/create/CreateAccount.tsx b/src/view/com/auth/create/CreateAccount.tsx
deleted file mode 100644
index 5d452736a..000000000
--- a/src/view/com/auth/create/CreateAccount.tsx
+++ /dev/null
@@ -1,219 +0,0 @@
-import React from 'react'
-import {
-  ActivityIndicator,
-  ScrollView,
-  StyleSheet,
-  TouchableOpacity,
-  View,
-} from 'react-native'
-import {useAnalytics} from 'lib/analytics/analytics'
-import {Text} from '../../util/text/Text'
-import {LoggedOutLayout} from 'view/com/util/layouts/LoggedOutLayout'
-import {s} from 'lib/styles'
-import {usePalette} from 'lib/hooks/usePalette'
-import {msg, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {useOnboardingDispatch} from '#/state/shell'
-import {useSessionApi} from '#/state/session'
-import {useCreateAccount, submit} from './state'
-import {useServiceQuery} from '#/state/queries/service'
-import {
-  usePreferencesSetBirthDateMutation,
-  useSetSaveFeedsMutation,
-  DEFAULT_PROD_FEEDS,
-} from '#/state/queries/preferences'
-import {FEEDBACK_FORM_URL, HITSLOP_10, IS_PROD} from '#/lib/constants'
-
-import {Step1} from './Step1'
-import {Step2} from './Step2'
-import {Step3} from './Step3'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
-import {TextLink} from '../../util/Link'
-
-export function CreateAccount({onPressBack}: {onPressBack: () => void}) {
-  const {screen} = useAnalytics()
-  const pal = usePalette('default')
-  const {_} = useLingui()
-  const [uiState, uiDispatch] = useCreateAccount()
-  const onboardingDispatch = useOnboardingDispatch()
-  const {createAccount} = useSessionApi()
-  const {mutate: setBirthDate} = usePreferencesSetBirthDateMutation()
-  const {mutate: setSavedFeeds} = useSetSaveFeedsMutation()
-  const {isTabletOrDesktop} = useWebMediaQueries()
-
-  React.useEffect(() => {
-    screen('CreateAccount')
-  }, [screen])
-
-  // fetch service info
-  // =
-
-  const {
-    data: serviceInfo,
-    isFetching: serviceInfoIsFetching,
-    error: serviceInfoError,
-    refetch: refetchServiceInfo,
-  } = useServiceQuery(uiState.serviceUrl)
-
-  React.useEffect(() => {
-    if (serviceInfo) {
-      uiDispatch({type: 'set-service-description', value: serviceInfo})
-      uiDispatch({type: 'set-error', value: ''})
-    } else if (serviceInfoError) {
-      uiDispatch({
-        type: 'set-error',
-        value: _(
-          msg`Unable to contact your service. Please check your Internet connection.`,
-        ),
-      })
-    }
-  }, [_, uiDispatch, serviceInfo, serviceInfoError])
-
-  // event handlers
-  // =
-
-  const onPressBackInner = React.useCallback(() => {
-    if (uiState.canBack) {
-      uiDispatch({type: 'back'})
-    } else {
-      onPressBack()
-    }
-  }, [uiState, uiDispatch, onPressBack])
-
-  const onPressNext = React.useCallback(async () => {
-    if (!uiState.canNext) {
-      return
-    }
-    if (uiState.step < 3) {
-      uiDispatch({type: 'next'})
-    } else {
-      try {
-        await submit({
-          onboardingDispatch,
-          createAccount,
-          uiState,
-          uiDispatch,
-          _,
-        })
-        setBirthDate({birthDate: uiState.birthDate})
-        if (IS_PROD(uiState.serviceUrl)) {
-          setSavedFeeds(DEFAULT_PROD_FEEDS)
-        }
-      } catch {
-        // dont need to handle here
-      }
-    }
-  }, [
-    uiState,
-    uiDispatch,
-    onboardingDispatch,
-    createAccount,
-    setBirthDate,
-    setSavedFeeds,
-    _,
-  ])
-
-  // rendering
-  // =
-
-  return (
-    <LoggedOutLayout
-      leadin=""
-      title={_(msg`Create Account`)}
-      description={_(msg`We're so excited to have you join us!`)}>
-      <ScrollView
-        testID="createAccount"
-        style={pal.view}
-        keyboardShouldPersistTaps="handled"
-        keyboardDismissMode="on-drag">
-        <View style={styles.stepContainer}>
-          {uiState.step === 1 && (
-            <Step1 uiState={uiState} uiDispatch={uiDispatch} />
-          )}
-          {uiState.step === 2 && (
-            <Step2 uiState={uiState} uiDispatch={uiDispatch} />
-          )}
-          {uiState.step === 3 && (
-            <Step3 uiState={uiState} uiDispatch={uiDispatch} />
-          )}
-        </View>
-        <View style={[s.flexRow, s.pl20, s.pr20]}>
-          <TouchableOpacity
-            onPress={onPressBackInner}
-            testID="backBtn"
-            accessibilityRole="button"
-            hitSlop={HITSLOP_10}>
-            <Text type="xl" style={pal.link}>
-              <Trans>Back</Trans>
-            </Text>
-          </TouchableOpacity>
-          <View style={s.flex1} />
-          {uiState.canNext ? (
-            <TouchableOpacity
-              testID="nextBtn"
-              onPress={onPressNext}
-              accessibilityRole="button"
-              hitSlop={HITSLOP_10}>
-              {uiState.isProcessing ? (
-                <ActivityIndicator />
-              ) : (
-                <Text type="xl-bold" style={[pal.link, s.pr5]}>
-                  <Trans>Next</Trans>
-                </Text>
-              )}
-            </TouchableOpacity>
-          ) : serviceInfoError ? (
-            <TouchableOpacity
-              testID="retryConnectBtn"
-              onPress={() => refetchServiceInfo()}
-              accessibilityRole="button"
-              accessibilityLabel={_(msg`Retry`)}
-              accessibilityHint=""
-              accessibilityLiveRegion="polite"
-              hitSlop={HITSLOP_10}>
-              <Text type="xl-bold" style={[pal.link, s.pr5]}>
-                <Trans>Retry</Trans>
-              </Text>
-            </TouchableOpacity>
-          ) : serviceInfoIsFetching ? (
-            <>
-              <ActivityIndicator color="#fff" />
-              <Text type="xl" style={[pal.text, s.pr5]}>
-                <Trans>Connecting...</Trans>
-              </Text>
-            </>
-          ) : undefined}
-        </View>
-
-        <View style={styles.stepContainer}>
-          <View
-            style={[
-              s.flexRow,
-              s.alignCenter,
-              pal.viewLight,
-              {borderRadius: 8, paddingHorizontal: 14, paddingVertical: 12},
-            ]}>
-            <Text type="md" style={pal.textLight}>
-              <Trans>Having trouble?</Trans>{' '}
-            </Text>
-            <TextLink
-              type="md"
-              style={pal.link}
-              text={_(msg`Contact support`)}
-              href={FEEDBACK_FORM_URL({email: uiState.email})}
-            />
-          </View>
-        </View>
-
-        <View style={{height: isTabletOrDesktop ? 50 : 400}} />
-      </ScrollView>
-    </LoggedOutLayout>
-  )
-}
-
-const styles = StyleSheet.create({
-  stepContainer: {
-    paddingHorizontal: 20,
-    paddingVertical: 20,
-  },
-})
diff --git a/src/view/com/auth/create/Policies.tsx b/src/view/com/auth/create/Policies.tsx
deleted file mode 100644
index 2c7d60818..000000000
--- a/src/view/com/auth/create/Policies.tsx
+++ /dev/null
@@ -1,114 +0,0 @@
-import React from 'react'
-import {StyleSheet, View} from 'react-native'
-import {
-  FontAwesomeIcon,
-  FontAwesomeIconStyle,
-} from '@fortawesome/react-native-fontawesome'
-import {ComAtprotoServerDescribeServer} from '@atproto/api'
-import {TextLink} from '../../util/Link'
-import {Text} from '../../util/text/Text'
-import {s, colors} from 'lib/styles'
-import {usePalette} from 'lib/hooks/usePalette'
-
-type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema
-
-export const Policies = ({
-  serviceDescription,
-  needsGuardian,
-}: {
-  serviceDescription: ServiceDescription
-  needsGuardian: boolean
-}) => {
-  const pal = usePalette('default')
-  if (!serviceDescription) {
-    return <View />
-  }
-  const tos = validWebLink(serviceDescription.links?.termsOfService)
-  const pp = validWebLink(serviceDescription.links?.privacyPolicy)
-  if (!tos && !pp) {
-    return (
-      <View style={[styles.policies, {flexDirection: 'row'}]}>
-        <View
-          style={[
-            styles.errorIcon,
-            {borderColor: pal.colors.text, marginTop: 1},
-          ]}>
-          <FontAwesomeIcon
-            icon="exclamation"
-            style={pal.textLight as FontAwesomeIconStyle}
-            size={10}
-          />
-        </View>
-        <Text style={[pal.textLight, s.pl5, s.flex1]}>
-          This service has not provided terms of service or a privacy policy.
-        </Text>
-      </View>
-    )
-  }
-  const els = []
-  if (tos) {
-    els.push(
-      <TextLink
-        key="tos"
-        href={tos}
-        text="Terms of Service"
-        style={[pal.link, s.underline]}
-      />,
-    )
-  }
-  if (pp) {
-    els.push(
-      <TextLink
-        key="pp"
-        href={pp}
-        text="Privacy Policy"
-        style={[pal.link, s.underline]}
-      />,
-    )
-  }
-  if (els.length === 2) {
-    els.splice(
-      1,
-      0,
-      <Text key="and" style={pal.textLight}>
-        {' '}
-        and{' '}
-      </Text>,
-    )
-  }
-  return (
-    <View style={styles.policies}>
-      <Text style={pal.textLight}>
-        By creating an account you agree to the {els}.
-      </Text>
-      {needsGuardian && (
-        <Text style={[pal.textLight, s.bold]}>
-          If you are not yet an adult according to the laws of your country,
-          your parent or legal guardian must read these Terms on your behalf.
-        </Text>
-      )}
-    </View>
-  )
-}
-
-function validWebLink(url?: string): string | undefined {
-  return url && (url.startsWith('http://') || url.startsWith('https://'))
-    ? url
-    : undefined
-}
-
-const styles = StyleSheet.create({
-  policies: {
-    flexDirection: 'column',
-    gap: 8,
-  },
-  errorIcon: {
-    borderWidth: 1,
-    borderColor: colors.white,
-    borderRadius: 30,
-    width: 16,
-    height: 16,
-    alignItems: 'center',
-    justifyContent: 'center',
-  },
-})
diff --git a/src/view/com/auth/create/Step1.tsx b/src/view/com/auth/create/Step1.tsx
deleted file mode 100644
index a7abbfaa8..000000000
--- a/src/view/com/auth/create/Step1.tsx
+++ /dev/null
@@ -1,283 +0,0 @@
-import React from 'react'
-import {
-  ActivityIndicator,
-  Keyboard,
-  StyleSheet,
-  TouchableOpacity,
-  TouchableWithoutFeedback,
-  View,
-} from 'react-native'
-import {CreateAccountState, CreateAccountDispatch, is18} from './state'
-import {Text} from 'view/com/util/text/Text'
-import {DateInput} from 'view/com/util/forms/DateInput'
-import {StepHeader} from './StepHeader'
-import {s} from 'lib/styles'
-import {usePalette} from 'lib/hooks/usePalette'
-import {TextInput} from '../util/TextInput'
-import {Policies} from './Policies'
-import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
-import {isWeb} from 'platform/detection'
-import {Trans, msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {useModalControls} from '#/state/modals'
-import {logger} from '#/logger'
-import {
-  FontAwesomeIcon,
-  FontAwesomeIconStyle,
-} from '@fortawesome/react-native-fontawesome'
-import {useDialogControl} from '#/components/Dialog'
-
-import {ServerInputDialog} from '../server-input'
-import {toNiceDomain} from '#/lib/strings/url-helpers'
-
-function sanitizeDate(date: Date): Date {
-  if (!date || date.toString() === 'Invalid Date') {
-    logger.error(`Create account: handled invalid date for birthDate`, {
-      hasDate: !!date,
-    })
-    return new Date()
-  }
-  return date
-}
-
-export function Step1({
-  uiState,
-  uiDispatch,
-}: {
-  uiState: CreateAccountState
-  uiDispatch: CreateAccountDispatch
-}) {
-  const pal = usePalette('default')
-  const {_} = useLingui()
-  const {openModal} = useModalControls()
-  const serverInputControl = useDialogControl()
-
-  const onPressSelectService = React.useCallback(() => {
-    serverInputControl.open()
-    Keyboard.dismiss()
-  }, [serverInputControl])
-
-  const onPressWaitlist = React.useCallback(() => {
-    openModal({name: 'waitlist'})
-  }, [openModal])
-
-  const birthDate = React.useMemo(() => {
-    return sanitizeDate(uiState.birthDate)
-  }, [uiState.birthDate])
-
-  return (
-    <View>
-      <ServerInputDialog
-        control={serverInputControl}
-        onSelect={url => uiDispatch({type: 'set-service-url', value: url})}
-      />
-      <StepHeader uiState={uiState} title={_(msg`Your account`)} />
-
-      <View style={s.pb20}>
-        <Text type="md-medium" style={[pal.text, s.mb2]}>
-          <Trans>Hosting provider</Trans>
-        </Text>
-        <View style={[pal.border, {borderWidth: 1, borderRadius: 6}]}>
-          <View
-            style={[
-              pal.borderDark,
-              {flexDirection: 'row', alignItems: 'center'},
-            ]}>
-            <FontAwesomeIcon
-              icon="globe"
-              style={[pal.textLight, {marginLeft: 14}]}
-            />
-            <TouchableOpacity
-              testID="selectServiceButton"
-              style={{
-                flexDirection: 'row',
-                flex: 1,
-                alignItems: 'center',
-              }}
-              onPress={onPressSelectService}
-              accessibilityRole="button"
-              accessibilityLabel={_(msg`Select service`)}
-              accessibilityHint={_(msg`Sets server for the Bluesky client`)}>
-              <Text
-                type="xl"
-                style={[
-                  pal.text,
-                  {
-                    flex: 1,
-                    paddingVertical: 10,
-                    paddingRight: 12,
-                    paddingLeft: 10,
-                  },
-                ]}>
-                {toNiceDomain(uiState.serviceUrl)}
-              </Text>
-              <View
-                style={[
-                  pal.btn,
-                  {
-                    flexDirection: 'row',
-                    alignItems: 'center',
-                    borderRadius: 6,
-                    paddingVertical: 6,
-                    paddingHorizontal: 8,
-                    marginHorizontal: 6,
-                  },
-                ]}>
-                <FontAwesomeIcon
-                  icon="pen"
-                  size={12}
-                  style={pal.textLight as FontAwesomeIconStyle}
-                />
-              </View>
-            </TouchableOpacity>
-          </View>
-        </View>
-      </View>
-
-      {!uiState.serviceDescription ? (
-        <ActivityIndicator />
-      ) : (
-        <>
-          {uiState.isInviteCodeRequired && (
-            <View style={s.pb20}>
-              <Text type="md-medium" style={[pal.text, s.mb2]}>
-                <Trans>Invite code</Trans>
-              </Text>
-              <TextInput
-                testID="inviteCodeInput"
-                icon="ticket"
-                placeholder={_(msg`Required for this provider`)}
-                value={uiState.inviteCode}
-                editable
-                onChange={value => uiDispatch({type: 'set-invite-code', value})}
-                accessibilityLabel={_(msg`Invite code`)}
-                accessibilityHint={_(msg`Input invite code to proceed`)}
-                autoCapitalize="none"
-                autoComplete="off"
-                autoCorrect={false}
-                autoFocus={true}
-              />
-            </View>
-          )}
-
-          {!uiState.inviteCode && uiState.isInviteCodeRequired ? (
-            <View style={[s.flexRow, s.alignCenter]}>
-              <Text style={pal.text}>
-                <Trans>Don't have an invite code?</Trans>{' '}
-              </Text>
-              <TouchableWithoutFeedback
-                onPress={onPressWaitlist}
-                accessibilityLabel={_(msg`Join the waitlist.`)}
-                accessibilityHint="">
-                <View style={styles.touchable}>
-                  <Text style={pal.link}>
-                    <Trans>Join the waitlist.</Trans>
-                  </Text>
-                </View>
-              </TouchableWithoutFeedback>
-            </View>
-          ) : (
-            <>
-              <View style={s.pb20}>
-                <Text
-                  type="md-medium"
-                  style={[pal.text, s.mb2]}
-                  nativeID="email">
-                  <Trans>Email address</Trans>
-                </Text>
-                <TextInput
-                  testID="emailInput"
-                  icon="envelope"
-                  placeholder={_(msg`Enter your email address`)}
-                  value={uiState.email}
-                  editable
-                  onChange={value => uiDispatch({type: 'set-email', value})}
-                  accessibilityLabel={_(msg`Email`)}
-                  accessibilityHint={_(msg`Input email for Bluesky account`)}
-                  accessibilityLabelledBy="email"
-                  autoCapitalize="none"
-                  autoComplete="email"
-                  autoCorrect={false}
-                  autoFocus={!uiState.isInviteCodeRequired}
-                />
-              </View>
-
-              <View style={s.pb20}>
-                <Text
-                  type="md-medium"
-                  style={[pal.text, s.mb2]}
-                  nativeID="password">
-                  <Trans>Password</Trans>
-                </Text>
-                <TextInput
-                  testID="passwordInput"
-                  icon="lock"
-                  placeholder={_(msg`Choose your password`)}
-                  value={uiState.password}
-                  editable
-                  secureTextEntry
-                  onChange={value => uiDispatch({type: 'set-password', value})}
-                  accessibilityLabel={_(msg`Password`)}
-                  accessibilityHint={_(msg`Set password`)}
-                  accessibilityLabelledBy="password"
-                  autoCapitalize="none"
-                  autoComplete="new-password"
-                  autoCorrect={false}
-                />
-              </View>
-
-              <View style={s.pb20}>
-                <Text
-                  type="md-medium"
-                  style={[pal.text, s.mb2]}
-                  nativeID="birthDate">
-                  <Trans>Your birth date</Trans>
-                </Text>
-                <DateInput
-                  handleAsUTC
-                  testID="birthdayInput"
-                  value={birthDate}
-                  onChange={value =>
-                    uiDispatch({type: 'set-birth-date', value})
-                  }
-                  buttonType="default-light"
-                  buttonStyle={[pal.border, styles.dateInputButton]}
-                  buttonLabelType="lg"
-                  accessibilityLabel={_(msg`Birthday`)}
-                  accessibilityHint={_(msg`Enter your birth date`)}
-                  accessibilityLabelledBy="birthDate"
-                />
-              </View>
-
-              {uiState.serviceDescription && (
-                <Policies
-                  serviceDescription={uiState.serviceDescription}
-                  needsGuardian={!is18(uiState)}
-                />
-              )}
-            </>
-          )}
-        </>
-      )}
-      {uiState.error ? (
-        <ErrorMessage message={uiState.error} style={styles.error} />
-      ) : undefined}
-    </View>
-  )
-}
-
-const styles = StyleSheet.create({
-  error: {
-    borderRadius: 6,
-    marginTop: 10,
-  },
-  dateInputButton: {
-    borderWidth: 1,
-    borderRadius: 6,
-    paddingVertical: 14,
-  },
-  // @ts-expect-error: Suppressing error due to incomplete `ViewStyle` type definition in react-native-web, missing `cursor` prop as discussed in https://github.com/necolas/react-native-web/issues/832.
-  touchable: {
-    ...(isWeb && {cursor: 'pointer'}),
-  },
-})
diff --git a/src/view/com/auth/create/Step2.tsx b/src/view/com/auth/create/Step2.tsx
deleted file mode 100644
index 2e16b13bb..000000000
--- a/src/view/com/auth/create/Step2.tsx
+++ /dev/null
@@ -1,307 +0,0 @@
-import React from 'react'
-import {
-  ActivityIndicator,
-  StyleSheet,
-  TouchableWithoutFeedback,
-  View,
-} from 'react-native'
-import RNPickerSelect from 'react-native-picker-select'
-import {
-  CreateAccountState,
-  CreateAccountDispatch,
-  requestVerificationCode,
-} from './state'
-import {Text} from 'view/com/util/text/Text'
-import {StepHeader} from './StepHeader'
-import {s} from 'lib/styles'
-import {usePalette} from 'lib/hooks/usePalette'
-import {TextInput} from '../util/TextInput'
-import {Button} from '../../util/forms/Button'
-import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
-import {isAndroid, isWeb} from 'platform/detection'
-import {Trans, msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
-import parsePhoneNumber from 'libphonenumber-js'
-import {COUNTRY_CODES} from '#/lib/country-codes'
-import {
-  FontAwesomeIcon,
-  FontAwesomeIconStyle,
-} from '@fortawesome/react-native-fontawesome'
-import {HITSLOP_10} from '#/lib/constants'
-
-export function Step2({
-  uiState,
-  uiDispatch,
-}: {
-  uiState: CreateAccountState
-  uiDispatch: CreateAccountDispatch
-}) {
-  const pal = usePalette('default')
-  const {_} = useLingui()
-  const {isMobile} = useWebMediaQueries()
-
-  const onPressRequest = React.useCallback(() => {
-    const phoneNumber = parsePhoneNumber(
-      uiState.verificationPhone,
-      uiState.phoneCountry,
-    )
-    if (phoneNumber && phoneNumber.isValid()) {
-      requestVerificationCode({uiState, uiDispatch, _})
-    } else {
-      uiDispatch({
-        type: 'set-error',
-        value: _(
-          msg`There's something wrong with this number. Please choose your country and enter your full phone number!`,
-        ),
-      })
-    }
-  }, [uiState, uiDispatch, _])
-
-  const onPressRetry = React.useCallback(() => {
-    uiDispatch({type: 'set-has-requested-verification-code', value: false})
-  }, [uiDispatch])
-
-  const phoneNumberFormatted = React.useMemo(
-    () =>
-      uiState.hasRequestedVerificationCode
-        ? parsePhoneNumber(
-            uiState.verificationPhone,
-            uiState.phoneCountry,
-          )?.formatInternational()
-        : '',
-    [
-      uiState.hasRequestedVerificationCode,
-      uiState.verificationPhone,
-      uiState.phoneCountry,
-    ],
-  )
-
-  return (
-    <View>
-      <StepHeader uiState={uiState} title={_(msg`SMS verification`)} />
-
-      {!uiState.hasRequestedVerificationCode ? (
-        <>
-          <View style={s.pb10}>
-            <Text
-              type="md-medium"
-              style={[pal.text, s.mb2]}
-              nativeID="phoneCountry">
-              <Trans>Country</Trans>
-            </Text>
-            <View
-              style={[
-                {position: 'relative'},
-                isAndroid && {
-                  borderWidth: 1,
-                  borderColor: pal.border.borderColor,
-                  borderRadius: 4,
-                },
-              ]}>
-              <RNPickerSelect
-                placeholder={{}}
-                value={uiState.phoneCountry}
-                onValueChange={value =>
-                  uiDispatch({type: 'set-phone-country', value})
-                }
-                items={COUNTRY_CODES.filter(l => Boolean(l.code2)).map(l => ({
-                  label: l.name,
-                  value: l.code2,
-                  key: l.code2,
-                }))}
-                style={{
-                  inputAndroid: {
-                    backgroundColor: pal.view.backgroundColor,
-                    color: pal.text.color,
-                    fontSize: 21,
-                    letterSpacing: 0.5,
-                    fontWeight: '500',
-                    paddingHorizontal: 14,
-                    paddingVertical: 8,
-                    borderRadius: 4,
-                  },
-                  inputIOS: {
-                    backgroundColor: pal.view.backgroundColor,
-                    color: pal.text.color,
-                    fontSize: 14,
-                    letterSpacing: 0.5,
-                    fontWeight: '500',
-                    paddingHorizontal: 14,
-                    paddingVertical: 8,
-                    borderWidth: 1,
-                    borderColor: pal.border.borderColor,
-                    borderRadius: 4,
-                  },
-                  inputWeb: {
-                    // @ts-ignore web only
-                    cursor: 'pointer',
-                    '-moz-appearance': 'none',
-                    '-webkit-appearance': 'none',
-                    appearance: 'none',
-                    outline: 0,
-                    borderWidth: 1,
-                    borderColor: pal.border.borderColor,
-                    backgroundColor: pal.view.backgroundColor,
-                    color: pal.text.color,
-                    fontSize: 14,
-                    letterSpacing: 0.5,
-                    fontWeight: '500',
-                    paddingHorizontal: 14,
-                    paddingVertical: 8,
-                    borderRadius: 4,
-                  },
-                }}
-                accessibilityLabel={_(msg`Select your phone's country`)}
-                accessibilityHint=""
-                accessibilityLabelledBy="phoneCountry"
-              />
-              <View
-                style={{
-                  position: 'absolute',
-                  top: 1,
-                  right: 1,
-                  bottom: 1,
-                  width: 40,
-                  pointerEvents: 'none',
-                  alignItems: 'center',
-                  justifyContent: 'center',
-                }}>
-                <FontAwesomeIcon
-                  icon="chevron-down"
-                  style={pal.text as FontAwesomeIconStyle}
-                />
-              </View>
-            </View>
-          </View>
-
-          <View style={s.pb20}>
-            <Text
-              type="md-medium"
-              style={[pal.text, s.mb2]}
-              nativeID="phoneNumber">
-              <Trans>Phone number</Trans>
-            </Text>
-            <TextInput
-              testID="phoneInput"
-              icon="phone"
-              placeholder={_(msg`Enter your phone number`)}
-              value={uiState.verificationPhone}
-              editable
-              onChange={value =>
-                uiDispatch({type: 'set-verification-phone', value})
-              }
-              accessibilityLabel={_(msg`Email`)}
-              accessibilityHint={_(
-                msg`Input phone number for SMS verification`,
-              )}
-              accessibilityLabelledBy="phoneNumber"
-              keyboardType="phone-pad"
-              autoCapitalize="none"
-              autoComplete="tel"
-              autoCorrect={false}
-              autoFocus={true}
-            />
-            <Text type="sm" style={[pal.textLight, s.mt5]}>
-              <Trans>
-                Please enter a phone number that can receive SMS text messages.
-              </Trans>
-            </Text>
-          </View>
-
-          <View style={isMobile ? {} : {flexDirection: 'row'}}>
-            {uiState.isProcessing ? (
-              <ActivityIndicator />
-            ) : (
-              <Button
-                testID="requestCodeBtn"
-                type="primary"
-                label={_(msg`Request code`)}
-                labelStyle={isMobile ? [s.flex1, s.textCenter, s.f17] : []}
-                style={
-                  isMobile ? {paddingVertical: 12, paddingHorizontal: 20} : {}
-                }
-                onPress={onPressRequest}
-              />
-            )}
-          </View>
-        </>
-      ) : (
-        <>
-          <View style={s.pb20}>
-            <View
-              style={[
-                s.flexRow,
-                s.mb5,
-                s.alignCenter,
-                {justifyContent: 'space-between'},
-              ]}>
-              <Text
-                type="md-medium"
-                style={pal.text}
-                nativeID="verificationCode">
-                <Trans>Verification code</Trans>{' '}
-              </Text>
-              <TouchableWithoutFeedback
-                onPress={onPressRetry}
-                accessibilityLabel={_(msg`Retry.`)}
-                accessibilityHint=""
-                hitSlop={HITSLOP_10}>
-                <View style={styles.touchable}>
-                  <Text
-                    type="md-medium"
-                    style={pal.link}
-                    nativeID="verificationCode">
-                    <Trans>Retry</Trans>
-                  </Text>
-                </View>
-              </TouchableWithoutFeedback>
-            </View>
-            <TextInput
-              testID="codeInput"
-              icon="hashtag"
-              placeholder={_(msg`XXXXXX`)}
-              value={uiState.verificationCode}
-              editable
-              onChange={value =>
-                uiDispatch({type: 'set-verification-code', value})
-              }
-              accessibilityLabel={_(msg`Email`)}
-              accessibilityHint={_(
-                msg`Input the verification code we have texted to you`,
-              )}
-              accessibilityLabelledBy="verificationCode"
-              keyboardType="phone-pad"
-              autoCapitalize="none"
-              autoComplete="one-time-code"
-              textContentType="oneTimeCode"
-              autoCorrect={false}
-              autoFocus={true}
-            />
-            <Text type="sm" style={[pal.textLight, s.mt5]}>
-              <Trans>
-                Please enter the verification code sent to{' '}
-                {phoneNumberFormatted}.
-              </Trans>
-            </Text>
-          </View>
-        </>
-      )}
-
-      {uiState.error ? (
-        <ErrorMessage message={uiState.error} style={styles.error} />
-      ) : undefined}
-    </View>
-  )
-}
-
-const styles = StyleSheet.create({
-  error: {
-    borderRadius: 6,
-    marginTop: 10,
-  },
-  // @ts-expect-error: Suppressing error due to incomplete `ViewStyle` type definition in react-native-web, missing `cursor` prop as discussed in https://github.com/necolas/react-native-web/issues/832.
-  touchable: {
-    ...(isWeb && {cursor: 'pointer'}),
-  },
-})
diff --git a/src/view/com/auth/create/Step3.tsx b/src/view/com/auth/create/Step3.tsx
deleted file mode 100644
index 3a52abf80..000000000
--- a/src/view/com/auth/create/Step3.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-import React from 'react'
-import {StyleSheet, View} from 'react-native'
-import {CreateAccountState, CreateAccountDispatch} from './state'
-import {Text} from 'view/com/util/text/Text'
-import {StepHeader} from './StepHeader'
-import {s} from 'lib/styles'
-import {TextInput} from '../util/TextInput'
-import {createFullHandle} from 'lib/strings/handles'
-import {usePalette} from 'lib/hooks/usePalette'
-import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
-import {msg, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-
-/** STEP 3: Your user handle
- * @field User handle
- */
-export function Step3({
-  uiState,
-  uiDispatch,
-}: {
-  uiState: CreateAccountState
-  uiDispatch: CreateAccountDispatch
-}) {
-  const pal = usePalette('default')
-  const {_} = useLingui()
-  return (
-    <View>
-      <StepHeader uiState={uiState} title={_(msg`Your user handle`)} />
-      <View style={s.pb10}>
-        <TextInput
-          testID="handleInput"
-          icon="at"
-          placeholder="e.g. alice"
-          value={uiState.handle}
-          editable
-          autoFocus
-          autoComplete="off"
-          autoCorrect={false}
-          onChange={value => uiDispatch({type: 'set-handle', value})}
-          // TODO: Add explicit text label
-          accessibilityLabel={_(msg`User handle`)}
-          accessibilityHint={_(msg`Input your user handle`)}
-        />
-        <Text type="lg" style={[pal.text, s.pl5, s.pt10]}>
-          <Trans>Your full handle will be</Trans>{' '}
-          <Text type="lg-bold" style={pal.text}>
-            @{createFullHandle(uiState.handle, uiState.userDomain)}
-          </Text>
-        </Text>
-      </View>
-      {uiState.error ? (
-        <ErrorMessage message={uiState.error} style={styles.error} />
-      ) : undefined}
-    </View>
-  )
-}
-
-const styles = StyleSheet.create({
-  error: {
-    borderRadius: 6,
-  },
-})
diff --git a/src/view/com/auth/create/StepHeader.tsx b/src/view/com/auth/create/StepHeader.tsx
deleted file mode 100644
index af6bf5478..000000000
--- a/src/view/com/auth/create/StepHeader.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import React from 'react'
-import {StyleSheet, View} from 'react-native'
-import {Text} from 'view/com/util/text/Text'
-import {usePalette} from 'lib/hooks/usePalette'
-import {Trans} from '@lingui/macro'
-import {CreateAccountState} from './state'
-
-export function StepHeader({
-  uiState,
-  title,
-  children,
-}: React.PropsWithChildren<{uiState: CreateAccountState; title: string}>) {
-  const pal = usePalette('default')
-  const numSteps = uiState.isPhoneVerificationRequired ? 3 : 2
-  return (
-    <View style={styles.container}>
-      <View>
-        <Text type="lg" style={[pal.textLight]}>
-          {uiState.step === 3 ? (
-            <Trans>Last step!</Trans>
-          ) : (
-            <Trans>
-              Step {uiState.step} of {numSteps}
-            </Trans>
-          )}
-        </Text>
-
-        <Text style={[pal.text]} type="title-xl">
-          {title}
-        </Text>
-      </View>
-      {children}
-    </View>
-  )
-}
-
-const styles = StyleSheet.create({
-  container: {
-    flexDirection: 'row',
-    justifyContent: 'space-between',
-    alignItems: 'center',
-    marginBottom: 20,
-  },
-})
diff --git a/src/view/com/auth/create/state.ts b/src/view/com/auth/create/state.ts
deleted file mode 100644
index e8a7cd4ed..000000000
--- a/src/view/com/auth/create/state.ts
+++ /dev/null
@@ -1,350 +0,0 @@
-import {useReducer} from 'react'
-import {
-  ComAtprotoServerDescribeServer,
-  ComAtprotoServerCreateAccount,
-  BskyAgent,
-} from '@atproto/api'
-import {I18nContext, useLingui} from '@lingui/react'
-import {msg} from '@lingui/macro'
-import * as EmailValidator from 'email-validator'
-import {getAge} from 'lib/strings/time'
-import {logger} from '#/logger'
-import {createFullHandle} from '#/lib/strings/handles'
-import {cleanError} from '#/lib/strings/errors'
-import {DispatchContext as OnboardingDispatchContext} from '#/state/shell/onboarding'
-import {ApiContext as SessionApiContext} from '#/state/session'
-import {DEFAULT_SERVICE} from '#/lib/constants'
-import parsePhoneNumber, {CountryCode} from 'libphonenumber-js'
-
-export type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema
-const DEFAULT_DATE = new Date(Date.now() - 60e3 * 60 * 24 * 365 * 20) // default to 20 years ago
-
-export type CreateAccountAction =
-  | {type: 'set-step'; value: number}
-  | {type: 'set-error'; value: string | undefined}
-  | {type: 'set-processing'; value: boolean}
-  | {type: 'set-service-url'; value: string}
-  | {type: 'set-service-description'; value: ServiceDescription | undefined}
-  | {type: 'set-user-domain'; value: string}
-  | {type: 'set-invite-code'; value: string}
-  | {type: 'set-email'; value: string}
-  | {type: 'set-password'; value: string}
-  | {type: 'set-phone-country'; value: CountryCode}
-  | {type: 'set-verification-phone'; value: string}
-  | {type: 'set-verification-code'; value: string}
-  | {type: 'set-has-requested-verification-code'; value: boolean}
-  | {type: 'set-handle'; value: string}
-  | {type: 'set-birth-date'; value: Date}
-  | {type: 'next'}
-  | {type: 'back'}
-
-export interface CreateAccountState {
-  // state
-  step: number
-  error: string | undefined
-  isProcessing: boolean
-  serviceUrl: string
-  serviceDescription: ServiceDescription | undefined
-  userDomain: string
-  inviteCode: string
-  email: string
-  password: string
-  phoneCountry: CountryCode
-  verificationPhone: string
-  verificationCode: string
-  hasRequestedVerificationCode: boolean
-  handle: string
-  birthDate: Date
-
-  // computed
-  canBack: boolean
-  canNext: boolean
-  isInviteCodeRequired: boolean
-  isPhoneVerificationRequired: boolean
-}
-
-export type CreateAccountDispatch = (action: CreateAccountAction) => void
-
-export function useCreateAccount() {
-  const {_} = useLingui()
-  return useReducer(createReducer({_}), {
-    step: 1,
-    error: undefined,
-    isProcessing: false,
-    serviceUrl: DEFAULT_SERVICE,
-    serviceDescription: undefined,
-    userDomain: '',
-    inviteCode: '',
-    email: '',
-    password: '',
-    phoneCountry: 'US',
-    verificationPhone: '',
-    verificationCode: '',
-    hasRequestedVerificationCode: false,
-    handle: '',
-    birthDate: DEFAULT_DATE,
-
-    canBack: false,
-    canNext: false,
-    isInviteCodeRequired: false,
-    isPhoneVerificationRequired: false,
-  })
-}
-
-export async function requestVerificationCode({
-  uiState,
-  uiDispatch,
-  _,
-}: {
-  uiState: CreateAccountState
-  uiDispatch: CreateAccountDispatch
-  _: I18nContext['_']
-}) {
-  const phoneNumber = parsePhoneNumber(
-    uiState.verificationPhone,
-    uiState.phoneCountry,
-  )?.number
-  if (!phoneNumber) {
-    return
-  }
-  uiDispatch({type: 'set-error', value: ''})
-  uiDispatch({type: 'set-processing', value: true})
-  uiDispatch({type: 'set-verification-phone', value: phoneNumber})
-  try {
-    const agent = new BskyAgent({service: uiState.serviceUrl})
-    await agent.com.atproto.temp.requestPhoneVerification({
-      phoneNumber,
-    })
-    uiDispatch({type: 'set-has-requested-verification-code', value: true})
-  } catch (e: any) {
-    logger.error(
-      `Failed to request sms verification code (${e.status} status)`,
-      {message: e},
-    )
-    uiDispatch({type: 'set-error', value: cleanError(e.toString())})
-  }
-  uiDispatch({type: 'set-processing', value: false})
-}
-
-export async function submit({
-  createAccount,
-  onboardingDispatch,
-  uiState,
-  uiDispatch,
-  _,
-}: {
-  createAccount: SessionApiContext['createAccount']
-  onboardingDispatch: OnboardingDispatchContext
-  uiState: CreateAccountState
-  uiDispatch: CreateAccountDispatch
-  _: I18nContext['_']
-}) {
-  if (!uiState.email) {
-    uiDispatch({type: 'set-step', value: 1})
-    return uiDispatch({
-      type: 'set-error',
-      value: _(msg`Please enter your email.`),
-    })
-  }
-  if (!EmailValidator.validate(uiState.email)) {
-    uiDispatch({type: 'set-step', value: 1})
-    return uiDispatch({
-      type: 'set-error',
-      value: _(msg`Your email appears to be invalid.`),
-    })
-  }
-  if (!uiState.password) {
-    uiDispatch({type: 'set-step', value: 1})
-    return uiDispatch({
-      type: 'set-error',
-      value: _(msg`Please choose your password.`),
-    })
-  }
-  if (
-    uiState.isPhoneVerificationRequired &&
-    (!uiState.verificationPhone || !uiState.verificationCode)
-  ) {
-    uiDispatch({type: 'set-step', value: 2})
-    return uiDispatch({
-      type: 'set-error',
-      value: _(msg`Please enter the code you received by SMS.`),
-    })
-  }
-  if (!uiState.handle) {
-    uiDispatch({type: 'set-step', value: 3})
-    return uiDispatch({
-      type: 'set-error',
-      value: _(msg`Please choose your handle.`),
-    })
-  }
-  uiDispatch({type: 'set-error', value: ''})
-  uiDispatch({type: 'set-processing', value: true})
-
-  try {
-    onboardingDispatch({type: 'start'}) // start now to avoid flashing the wrong view
-    await createAccount({
-      service: uiState.serviceUrl,
-      email: uiState.email,
-      handle: createFullHandle(uiState.handle, uiState.userDomain),
-      password: uiState.password,
-      inviteCode: uiState.inviteCode.trim(),
-      verificationPhone: uiState.verificationPhone.trim(),
-      verificationCode: uiState.verificationCode.trim(),
-    })
-  } catch (e: any) {
-    onboardingDispatch({type: 'skip'}) // undo starting the onboard
-    let errMsg = e.toString()
-    if (e instanceof ComAtprotoServerCreateAccount.InvalidInviteCodeError) {
-      errMsg = _(
-        msg`Invite code not accepted. Check that you input it correctly and try again.`,
-      )
-      uiDispatch({type: 'set-step', value: 1})
-    } else if (e.error === 'InvalidPhoneVerification') {
-      uiDispatch({type: 'set-step', value: 2})
-    }
-
-    if ([400, 429].includes(e.status)) {
-      logger.warn('Failed to create account', {message: e})
-    } else {
-      logger.error(`Failed to create account (${e.status} status)`, {
-        message: e,
-      })
-    }
-
-    uiDispatch({type: 'set-processing', value: false})
-    uiDispatch({type: 'set-error', value: cleanError(errMsg)})
-    throw e
-  }
-}
-
-export function is13(state: CreateAccountState) {
-  return getAge(state.birthDate) >= 13
-}
-
-export function is18(state: CreateAccountState) {
-  return getAge(state.birthDate) >= 18
-}
-
-function createReducer({_}: {_: I18nContext['_']}) {
-  return function reducer(
-    state: CreateAccountState,
-    action: CreateAccountAction,
-  ): CreateAccountState {
-    switch (action.type) {
-      case 'set-step': {
-        return compute({...state, step: action.value})
-      }
-      case 'set-error': {
-        return compute({...state, error: action.value})
-      }
-      case 'set-processing': {
-        return compute({...state, isProcessing: action.value})
-      }
-      case 'set-service-url': {
-        return compute({
-          ...state,
-          serviceUrl: action.value,
-          serviceDescription:
-            state.serviceUrl !== action.value
-              ? undefined
-              : state.serviceDescription,
-        })
-      }
-      case 'set-service-description': {
-        return compute({
-          ...state,
-          serviceDescription: action.value,
-          userDomain: action.value?.availableUserDomains[0] || '',
-        })
-      }
-      case 'set-user-domain': {
-        return compute({...state, userDomain: action.value})
-      }
-      case 'set-invite-code': {
-        return compute({...state, inviteCode: action.value})
-      }
-      case 'set-email': {
-        return compute({...state, email: action.value})
-      }
-      case 'set-password': {
-        return compute({...state, password: action.value})
-      }
-      case 'set-phone-country': {
-        return compute({...state, phoneCountry: action.value})
-      }
-      case 'set-verification-phone': {
-        return compute({
-          ...state,
-          verificationPhone: action.value,
-          hasRequestedVerificationCode: false,
-        })
-      }
-      case 'set-verification-code': {
-        return compute({...state, verificationCode: action.value.trim()})
-      }
-      case 'set-has-requested-verification-code': {
-        return compute({...state, hasRequestedVerificationCode: action.value})
-      }
-      case 'set-handle': {
-        return compute({...state, handle: action.value})
-      }
-      case 'set-birth-date': {
-        return compute({...state, birthDate: action.value})
-      }
-      case 'next': {
-        if (state.step === 1) {
-          if (!is13(state)) {
-            return compute({
-              ...state,
-              error: _(
-                msg`Unfortunately, you do not meet the requirements to create an account.`,
-              ),
-            })
-          }
-        }
-        let increment = 1
-        if (state.step === 1 && !state.isPhoneVerificationRequired) {
-          increment = 2
-        }
-        return compute({...state, error: '', step: state.step + increment})
-      }
-      case 'back': {
-        let decrement = 1
-        if (state.step === 3 && !state.isPhoneVerificationRequired) {
-          decrement = 2
-        }
-        return compute({...state, error: '', step: state.step - decrement})
-      }
-    }
-  }
-}
-
-function compute(state: CreateAccountState): CreateAccountState {
-  let canNext = true
-  if (state.step === 1) {
-    canNext =
-      !!state.serviceDescription &&
-      (!state.isInviteCodeRequired || !!state.inviteCode) &&
-      !!state.email &&
-      !!state.password
-  } else if (state.step === 2) {
-    canNext =
-      !state.isPhoneVerificationRequired ||
-      (!!state.verificationPhone &&
-        isValidVerificationCode(state.verificationCode))
-  } else if (state.step === 3) {
-    canNext = !!state.handle
-  }
-  return {
-    ...state,
-    canBack: state.step > 1,
-    canNext,
-    isInviteCodeRequired: !!state.serviceDescription?.inviteCodeRequired,
-    isPhoneVerificationRequired:
-      !!state.serviceDescription?.phoneVerificationRequired,
-  }
-}
-
-function isValidVerificationCode(str: string): boolean {
-  return /[0-9]{6}/.test(str)
-}
diff --git a/src/view/com/auth/login/ChooseAccountForm.tsx b/src/view/com/auth/login/ChooseAccountForm.tsx
deleted file mode 100644
index 32cd8315d..000000000
--- a/src/view/com/auth/login/ChooseAccountForm.tsx
+++ /dev/null
@@ -1,158 +0,0 @@
-import React from 'react'
-import {ScrollView, TouchableOpacity, View} from 'react-native'
-import {
-  FontAwesomeIcon,
-  FontAwesomeIconStyle,
-} from '@fortawesome/react-native-fontawesome'
-import {useAnalytics} from 'lib/analytics/analytics'
-import {Text} from '../../util/text/Text'
-import {UserAvatar} from '../../util/UserAvatar'
-import {s, colors} from 'lib/styles'
-import {usePalette} from 'lib/hooks/usePalette'
-import {Trans, msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {styles} from './styles'
-import {useSession, useSessionApi, SessionAccount} from '#/state/session'
-import {useProfileQuery} from '#/state/queries/profile'
-import {useLoggedOutViewControls} from '#/state/shell/logged-out'
-import * as Toast from '#/view/com/util/Toast'
-
-function AccountItem({
-  account,
-  onSelect,
-  isCurrentAccount,
-}: {
-  account: SessionAccount
-  onSelect: (account: SessionAccount) => void
-  isCurrentAccount: boolean
-}) {
-  const pal = usePalette('default')
-  const {_} = useLingui()
-  const {data: profile} = useProfileQuery({did: account.did})
-
-  const onPress = React.useCallback(() => {
-    onSelect(account)
-  }, [account, onSelect])
-
-  return (
-    <TouchableOpacity
-      testID={`chooseAccountBtn-${account.handle}`}
-      key={account.did}
-      style={[pal.view, pal.border, styles.account]}
-      onPress={onPress}
-      accessibilityRole="button"
-      accessibilityLabel={_(msg`Sign in as ${account.handle}`)}
-      accessibilityHint={_(msg`Double tap to sign in`)}>
-      <View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
-        <View style={s.p10}>
-          <UserAvatar avatar={profile?.avatar} size={30} />
-        </View>
-        <Text style={styles.accountText}>
-          <Text type="lg-bold" style={pal.text}>
-            {profile?.displayName || account.handle}{' '}
-          </Text>
-          <Text type="lg" style={[pal.textLight]}>
-            {account.handle}
-          </Text>
-        </Text>
-        {isCurrentAccount ? (
-          <FontAwesomeIcon
-            icon="check"
-            size={16}
-            style={[{color: colors.green3} as FontAwesomeIconStyle, s.mr10]}
-          />
-        ) : (
-          <FontAwesomeIcon
-            icon="angle-right"
-            size={16}
-            style={[pal.text, s.mr10]}
-          />
-        )}
-      </View>
-    </TouchableOpacity>
-  )
-}
-export const ChooseAccountForm = ({
-  onSelectAccount,
-  onPressBack,
-}: {
-  onSelectAccount: (account?: SessionAccount) => void
-  onPressBack: () => void
-}) => {
-  const {track, screen} = useAnalytics()
-  const pal = usePalette('default')
-  const {_} = useLingui()
-  const {accounts, currentAccount} = useSession()
-  const {initSession} = useSessionApi()
-  const {setShowLoggedOut} = useLoggedOutViewControls()
-
-  React.useEffect(() => {
-    screen('Choose Account')
-  }, [screen])
-
-  const onSelect = React.useCallback(
-    async (account: SessionAccount) => {
-      if (account.accessJwt) {
-        if (account.did === currentAccount?.did) {
-          setShowLoggedOut(false)
-          Toast.show(_(msg`Already signed in as @${account.handle}`))
-        } else {
-          await initSession(account)
-          track('Sign In', {resumedSession: true})
-          setTimeout(() => {
-            Toast.show(_(msg`Signed in as @${account.handle}`))
-          }, 100)
-        }
-      } else {
-        onSelectAccount(account)
-      }
-    },
-    [currentAccount, track, initSession, onSelectAccount, setShowLoggedOut, _],
-  )
-
-  return (
-    <ScrollView testID="chooseAccountForm" style={styles.maxHeight}>
-      <Text
-        type="2xl-medium"
-        style={[pal.text, styles.groupLabel, s.mt5, s.mb10]}>
-        <Trans>Sign in as...</Trans>
-      </Text>
-      {accounts.map(account => (
-        <AccountItem
-          key={account.did}
-          account={account}
-          onSelect={onSelect}
-          isCurrentAccount={account.did === currentAccount?.did}
-        />
-      ))}
-      <TouchableOpacity
-        testID="chooseNewAccountBtn"
-        style={[pal.view, pal.border, styles.account, styles.accountLast]}
-        onPress={() => onSelectAccount(undefined)}
-        accessibilityRole="button"
-        accessibilityLabel={_(msg`Login to account that is not listed`)}
-        accessibilityHint="">
-        <View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
-          <Text style={[styles.accountText, styles.accountTextOther]}>
-            <Text type="lg" style={pal.text}>
-              <Trans>Other account</Trans>
-            </Text>
-          </Text>
-          <FontAwesomeIcon
-            icon="angle-right"
-            size={16}
-            style={[pal.text, s.mr10]}
-          />
-        </View>
-      </TouchableOpacity>
-      <View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
-        <TouchableOpacity onPress={onPressBack} accessibilityRole="button">
-          <Text type="xl" style={[pal.link, s.pl5]}>
-            <Trans>Back</Trans>
-          </Text>
-        </TouchableOpacity>
-        <View style={s.flex1} />
-      </View>
-    </ScrollView>
-  )
-}
diff --git a/src/view/com/auth/login/ForgotPasswordForm.tsx b/src/view/com/auth/login/ForgotPasswordForm.tsx
deleted file mode 100644
index 322da2b8f..000000000
--- a/src/view/com/auth/login/ForgotPasswordForm.tsx
+++ /dev/null
@@ -1,228 +0,0 @@
-import React, {useState, useEffect} from 'react'
-import {
-  ActivityIndicator,
-  Keyboard,
-  TextInput,
-  TouchableOpacity,
-  View,
-} from 'react-native'
-import {
-  FontAwesomeIcon,
-  FontAwesomeIconStyle,
-} from '@fortawesome/react-native-fontawesome'
-import {ComAtprotoServerDescribeServer} from '@atproto/api'
-import * as EmailValidator from 'email-validator'
-import {BskyAgent} from '@atproto/api'
-import {useAnalytics} from 'lib/analytics/analytics'
-import {Text} from '../../util/text/Text'
-import {s} from 'lib/styles'
-import {toNiceDomain} from 'lib/strings/url-helpers'
-import {isNetworkError} from 'lib/strings/errors'
-import {usePalette} from 'lib/hooks/usePalette'
-import {useTheme} from 'lib/ThemeContext'
-import {cleanError} from 'lib/strings/errors'
-import {logger} from '#/logger'
-import {Trans, msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {styles} from './styles'
-import {useDialogControl} from '#/components/Dialog'
-
-import {ServerInputDialog} from '../server-input'
-
-type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema
-
-export const ForgotPasswordForm = ({
-  error,
-  serviceUrl,
-  serviceDescription,
-  setError,
-  setServiceUrl,
-  onPressBack,
-  onEmailSent,
-}: {
-  error: string
-  serviceUrl: string
-  serviceDescription: ServiceDescription | undefined
-  setError: (v: string) => void
-  setServiceUrl: (v: string) => void
-  onPressBack: () => void
-  onEmailSent: () => void
-}) => {
-  const pal = usePalette('default')
-  const theme = useTheme()
-  const [isProcessing, setIsProcessing] = useState<boolean>(false)
-  const [email, setEmail] = useState<string>('')
-  const {screen} = useAnalytics()
-  const {_} = useLingui()
-  const serverInputControl = useDialogControl()
-
-  useEffect(() => {
-    screen('Signin:ForgotPassword')
-  }, [screen])
-
-  const onPressSelectService = React.useCallback(() => {
-    serverInputControl.open()
-    Keyboard.dismiss()
-  }, [serverInputControl])
-
-  const onPressNext = async () => {
-    if (!EmailValidator.validate(email)) {
-      return setError(_(msg`Your email appears to be invalid.`))
-    }
-
-    setError('')
-    setIsProcessing(true)
-
-    try {
-      const agent = new BskyAgent({service: serviceUrl})
-      await agent.com.atproto.server.requestPasswordReset({email})
-      onEmailSent()
-    } catch (e: any) {
-      const errMsg = e.toString()
-      logger.warn('Failed to request password reset', {error: e})
-      setIsProcessing(false)
-      if (isNetworkError(e)) {
-        setError(
-          _(
-            msg`Unable to contact your service. Please check your Internet connection.`,
-          ),
-        )
-      } else {
-        setError(cleanError(errMsg))
-      }
-    }
-  }
-
-  return (
-    <>
-      <View>
-        <ServerInputDialog
-          control={serverInputControl}
-          onSelect={setServiceUrl}
-        />
-        <Text type="title-lg" style={[pal.text, styles.screenTitle]}>
-          <Trans>Reset password</Trans>
-        </Text>
-        <Text type="md" style={[pal.text, styles.instructions]}>
-          <Trans>
-            Enter the email you used to create your account. We'll send you a
-            "reset code" so you can set a new password.
-          </Trans>
-        </Text>
-        <View
-          testID="forgotPasswordView"
-          style={[pal.borderDark, pal.view, styles.group]}>
-          <TouchableOpacity
-            testID="forgotPasswordSelectServiceButton"
-            style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}
-            onPress={onPressSelectService}
-            accessibilityRole="button"
-            accessibilityLabel={_(msg`Hosting provider`)}
-            accessibilityHint={_(
-              msg`Sets hosting provider for password reset`,
-            )}>
-            <FontAwesomeIcon
-              icon="globe"
-              style={[pal.textLight, styles.groupContentIcon]}
-            />
-            <Text style={[pal.text, styles.textInput]} numberOfLines={1}>
-              {toNiceDomain(serviceUrl)}
-            </Text>
-            <View style={[pal.btn, styles.textBtnFakeInnerBtn]}>
-              <FontAwesomeIcon
-                icon="pen"
-                size={12}
-                style={pal.text as FontAwesomeIconStyle}
-              />
-            </View>
-          </TouchableOpacity>
-          <View style={[pal.borderDark, styles.groupContent]}>
-            <FontAwesomeIcon
-              icon="envelope"
-              style={[pal.textLight, styles.groupContentIcon]}
-            />
-            <TextInput
-              testID="forgotPasswordEmail"
-              style={[pal.text, styles.textInput]}
-              placeholder={_(msg`Email address`)}
-              placeholderTextColor={pal.colors.textLight}
-              autoCapitalize="none"
-              autoFocus
-              autoCorrect={false}
-              keyboardAppearance={theme.colorScheme}
-              value={email}
-              onChangeText={setEmail}
-              editable={!isProcessing}
-              accessibilityLabel={_(msg`Email`)}
-              accessibilityHint={_(msg`Sets email for password reset`)}
-            />
-          </View>
-        </View>
-        {error ? (
-          <View style={styles.error}>
-            <View style={styles.errorIcon}>
-              <FontAwesomeIcon icon="exclamation" style={s.white} size={10} />
-            </View>
-            <View style={s.flex1}>
-              <Text style={[s.white, s.bold]}>{error}</Text>
-            </View>
-          </View>
-        ) : undefined}
-        <View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
-          <TouchableOpacity onPress={onPressBack} accessibilityRole="button">
-            <Text type="xl" style={[pal.link, s.pl5]}>
-              <Trans>Back</Trans>
-            </Text>
-          </TouchableOpacity>
-          <View style={s.flex1} />
-          {!serviceDescription || isProcessing ? (
-            <ActivityIndicator />
-          ) : !email ? (
-            <Text type="xl-bold" style={[pal.link, s.pr5, styles.dimmed]}>
-              <Trans>Next</Trans>
-            </Text>
-          ) : (
-            <TouchableOpacity
-              testID="newPasswordButton"
-              onPress={onPressNext}
-              accessibilityRole="button"
-              accessibilityLabel={_(msg`Go to next`)}
-              accessibilityHint={_(msg`Navigates to the next screen`)}>
-              <Text type="xl-bold" style={[pal.link, s.pr5]}>
-                <Trans>Next</Trans>
-              </Text>
-            </TouchableOpacity>
-          )}
-          {!serviceDescription || isProcessing ? (
-            <Text type="xl" style={[pal.textLight, s.pl10]}>
-              <Trans>Processing...</Trans>
-            </Text>
-          ) : undefined}
-        </View>
-        <View
-          style={[
-            s.flexRow,
-            s.alignCenter,
-            s.mt20,
-            s.mb20,
-            pal.border,
-            s.borderBottom1,
-            {alignSelf: 'center', width: '90%'},
-          ]}
-        />
-        <View style={[s.flexRow, s.justifyCenter]}>
-          <TouchableOpacity
-            testID="skipSendEmailButton"
-            onPress={onEmailSent}
-            accessibilityRole="button"
-            accessibilityLabel={_(msg`Go to next`)}
-            accessibilityHint={_(msg`Navigates to the next screen`)}>
-            <Text type="xl" style={[pal.link, s.pr5]}>
-              <Trans>Already have a code?</Trans>
-            </Text>
-          </TouchableOpacity>
-        </View>
-      </View>
-    </>
-  )
-}
diff --git a/src/view/com/auth/login/Login.tsx b/src/view/com/auth/login/Login.tsx
deleted file mode 100644
index bc931ac04..000000000
--- a/src/view/com/auth/login/Login.tsx
+++ /dev/null
@@ -1,164 +0,0 @@
-import React, {useState, useEffect} from 'react'
-import {KeyboardAvoidingView} from 'react-native'
-import {useAnalytics} from 'lib/analytics/analytics'
-import {LoggedOutLayout} from 'view/com/util/layouts/LoggedOutLayout'
-import {DEFAULT_SERVICE} from '#/lib/constants'
-import {usePalette} from 'lib/hooks/usePalette'
-import {logger} from '#/logger'
-import {ChooseAccountForm} from './ChooseAccountForm'
-import {LoginForm} from './LoginForm'
-import {ForgotPasswordForm} from './ForgotPasswordForm'
-import {SetNewPasswordForm} from './SetNewPasswordForm'
-import {PasswordUpdatedForm} from './PasswordUpdatedForm'
-import {useLingui} from '@lingui/react'
-import {msg} from '@lingui/macro'
-import {useSession, SessionAccount} from '#/state/session'
-import {useServiceQuery} from '#/state/queries/service'
-import {useLoggedOutView} from '#/state/shell/logged-out'
-
-enum Forms {
-  Login,
-  ChooseAccount,
-  ForgotPassword,
-  SetNewPassword,
-  PasswordUpdated,
-}
-
-export const Login = ({onPressBack}: {onPressBack: () => void}) => {
-  const {_} = useLingui()
-  const pal = usePalette('default')
-
-  const {accounts} = useSession()
-  const {track} = useAnalytics()
-  const {requestedAccountSwitchTo} = useLoggedOutView()
-  const requestedAccount = accounts.find(
-    a => a.did === requestedAccountSwitchTo,
-  )
-
-  const [error, setError] = useState<string>('')
-  const [serviceUrl, setServiceUrl] = useState<string>(
-    requestedAccount?.service || DEFAULT_SERVICE,
-  )
-  const [initialHandle, setInitialHandle] = useState<string>(
-    requestedAccount?.handle || '',
-  )
-  const [currentForm, setCurrentForm] = useState<Forms>(
-    requestedAccount
-      ? Forms.Login
-      : accounts.length
-      ? Forms.ChooseAccount
-      : Forms.Login,
-  )
-
-  const {
-    data: serviceDescription,
-    error: serviceError,
-    refetch: refetchService,
-  } = useServiceQuery(serviceUrl)
-
-  const onSelectAccount = (account?: SessionAccount) => {
-    if (account?.service) {
-      setServiceUrl(account.service)
-    }
-    setInitialHandle(account?.handle || '')
-    setCurrentForm(Forms.Login)
-  }
-
-  const gotoForm = (form: Forms) => () => {
-    setError('')
-    setCurrentForm(form)
-  }
-
-  useEffect(() => {
-    if (serviceError) {
-      setError(
-        _(
-          msg`Unable to contact your service. Please check your Internet connection.`,
-        ),
-      )
-      logger.warn(`Failed to fetch service description for ${serviceUrl}`, {
-        error: String(serviceError),
-      })
-    } else {
-      setError('')
-    }
-  }, [serviceError, serviceUrl, _])
-
-  const onPressRetryConnect = () => refetchService()
-  const onPressForgotPassword = () => {
-    track('Signin:PressedForgotPassword')
-    setCurrentForm(Forms.ForgotPassword)
-  }
-
-  return (
-    <KeyboardAvoidingView testID="signIn" behavior="padding" style={pal.view}>
-      {currentForm === Forms.Login ? (
-        <LoggedOutLayout
-          leadin=""
-          title={_(msg`Sign in`)}
-          description={_(msg`Enter your username and password`)}>
-          <LoginForm
-            error={error}
-            serviceUrl={serviceUrl}
-            serviceDescription={serviceDescription}
-            initialHandle={initialHandle}
-            setError={setError}
-            setServiceUrl={setServiceUrl}
-            onPressBack={onPressBack}
-            onPressForgotPassword={onPressForgotPassword}
-            onPressRetryConnect={onPressRetryConnect}
-          />
-        </LoggedOutLayout>
-      ) : undefined}
-      {currentForm === Forms.ChooseAccount ? (
-        <LoggedOutLayout
-          leadin=""
-          title={_(msg`Sign in as...`)}
-          description={_(msg`Select from an existing account`)}>
-          <ChooseAccountForm
-            onSelectAccount={onSelectAccount}
-            onPressBack={onPressBack}
-          />
-        </LoggedOutLayout>
-      ) : undefined}
-      {currentForm === Forms.ForgotPassword ? (
-        <LoggedOutLayout
-          leadin=""
-          title={_(msg`Forgot Password`)}
-          description={_(msg`Let's get your password reset!`)}>
-          <ForgotPasswordForm
-            error={error}
-            serviceUrl={serviceUrl}
-            serviceDescription={serviceDescription}
-            setError={setError}
-            setServiceUrl={setServiceUrl}
-            onPressBack={gotoForm(Forms.Login)}
-            onEmailSent={gotoForm(Forms.SetNewPassword)}
-          />
-        </LoggedOutLayout>
-      ) : undefined}
-      {currentForm === Forms.SetNewPassword ? (
-        <LoggedOutLayout
-          leadin=""
-          title={_(msg`Forgot Password`)}
-          description={_(msg`Let's get your password reset!`)}>
-          <SetNewPasswordForm
-            error={error}
-            serviceUrl={serviceUrl}
-            setError={setError}
-            onPressBack={gotoForm(Forms.ForgotPassword)}
-            onPasswordSet={gotoForm(Forms.PasswordUpdated)}
-          />
-        </LoggedOutLayout>
-      ) : undefined}
-      {currentForm === Forms.PasswordUpdated ? (
-        <LoggedOutLayout
-          leadin=""
-          title={_(msg`Password updated`)}
-          description={_(msg`You can now sign in with your new password.`)}>
-          <PasswordUpdatedForm onPressNext={gotoForm(Forms.Login)} />
-        </LoggedOutLayout>
-      ) : undefined}
-    </KeyboardAvoidingView>
-  )
-}
diff --git a/src/view/com/auth/login/LoginForm.tsx b/src/view/com/auth/login/LoginForm.tsx
deleted file mode 100644
index e480de7a4..000000000
--- a/src/view/com/auth/login/LoginForm.tsx
+++ /dev/null
@@ -1,298 +0,0 @@
-import React, {useState, useRef} from 'react'
-import {
-  ActivityIndicator,
-  Keyboard,
-  TextInput,
-  TouchableOpacity,
-  View,
-} from 'react-native'
-import {
-  FontAwesomeIcon,
-  FontAwesomeIconStyle,
-} from '@fortawesome/react-native-fontawesome'
-import {ComAtprotoServerDescribeServer} from '@atproto/api'
-import {useAnalytics} from 'lib/analytics/analytics'
-import {Text} from '../../util/text/Text'
-import {s} from 'lib/styles'
-import {createFullHandle} from 'lib/strings/handles'
-import {toNiceDomain} from 'lib/strings/url-helpers'
-import {isNetworkError} from 'lib/strings/errors'
-import {usePalette} from 'lib/hooks/usePalette'
-import {useTheme} from 'lib/ThemeContext'
-import {useSessionApi} from '#/state/session'
-import {cleanError} from 'lib/strings/errors'
-import {logger} from '#/logger'
-import {Trans, msg} from '@lingui/macro'
-import {styles} from './styles'
-import {useLingui} from '@lingui/react'
-import {useDialogControl} from '#/components/Dialog'
-
-import {ServerInputDialog} from '../server-input'
-
-type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema
-
-export const LoginForm = ({
-  error,
-  serviceUrl,
-  serviceDescription,
-  initialHandle,
-  setError,
-  setServiceUrl,
-  onPressRetryConnect,
-  onPressBack,
-  onPressForgotPassword,
-}: {
-  error: string
-  serviceUrl: string
-  serviceDescription: ServiceDescription | undefined
-  initialHandle: string
-  setError: (v: string) => void
-  setServiceUrl: (v: string) => void
-  onPressRetryConnect: () => void
-  onPressBack: () => void
-  onPressForgotPassword: () => void
-}) => {
-  const {track} = useAnalytics()
-  const pal = usePalette('default')
-  const theme = useTheme()
-  const [isProcessing, setIsProcessing] = useState<boolean>(false)
-  const [identifier, setIdentifier] = useState<string>(initialHandle)
-  const [password, setPassword] = useState<string>('')
-  const passwordInputRef = useRef<TextInput>(null)
-  const {_} = useLingui()
-  const {login} = useSessionApi()
-  const serverInputControl = useDialogControl()
-
-  const onPressSelectService = () => {
-    serverInputControl.open()
-    Keyboard.dismiss()
-    track('Signin:PressedSelectService')
-  }
-
-  const onPressNext = async () => {
-    Keyboard.dismiss()
-    setError('')
-    setIsProcessing(true)
-
-    try {
-      // try to guess the handle if the user just gave their own username
-      let fullIdent = identifier
-      if (
-        !identifier.includes('@') && // not an email
-        !identifier.includes('.') && // not a domain
-        serviceDescription &&
-        serviceDescription.availableUserDomains.length > 0
-      ) {
-        let matched = false
-        for (const domain of serviceDescription.availableUserDomains) {
-          if (fullIdent.endsWith(domain)) {
-            matched = true
-          }
-        }
-        if (!matched) {
-          fullIdent = createFullHandle(
-            identifier,
-            serviceDescription.availableUserDomains[0],
-          )
-        }
-      }
-
-      // TODO remove double login
-      await login({
-        service: serviceUrl,
-        identifier: fullIdent,
-        password,
-      })
-    } catch (e: any) {
-      const errMsg = e.toString()
-      setIsProcessing(false)
-      if (errMsg.includes('Authentication Required')) {
-        logger.info('Failed to login due to invalid credentials', {
-          error: errMsg,
-        })
-        setError(_(msg`Invalid username or password`))
-      } else if (isNetworkError(e)) {
-        logger.warn('Failed to login due to network error', {error: errMsg})
-        setError(
-          _(
-            msg`Unable to contact your service. Please check your Internet connection.`,
-          ),
-        )
-      } else {
-        logger.warn('Failed to login', {error: errMsg})
-        setError(cleanError(errMsg))
-      }
-    }
-  }
-
-  const isReady = !!serviceDescription && !!identifier && !!password
-  return (
-    <View testID="loginForm">
-      <ServerInputDialog
-        control={serverInputControl}
-        onSelect={setServiceUrl}
-      />
-
-      <Text type="sm-bold" style={[pal.text, styles.groupLabel]}>
-        <Trans>Sign into</Trans>
-      </Text>
-      <View style={[pal.borderDark, styles.group]}>
-        <View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
-          <FontAwesomeIcon
-            icon="globe"
-            style={[pal.textLight, styles.groupContentIcon]}
-          />
-          <TouchableOpacity
-            testID="loginSelectServiceButton"
-            style={styles.textBtn}
-            onPress={onPressSelectService}
-            accessibilityRole="button"
-            accessibilityLabel={_(msg`Select service`)}
-            accessibilityHint={_(msg`Sets server for the Bluesky client`)}>
-            <Text type="xl" style={[pal.text, styles.textBtnLabel]}>
-              {toNiceDomain(serviceUrl)}
-            </Text>
-            <View style={[pal.btn, styles.textBtnFakeInnerBtn]}>
-              <FontAwesomeIcon
-                icon="pen"
-                size={12}
-                style={pal.textLight as FontAwesomeIconStyle}
-              />
-            </View>
-          </TouchableOpacity>
-        </View>
-      </View>
-      <Text type="sm-bold" style={[pal.text, styles.groupLabel]}>
-        <Trans>Account</Trans>
-      </Text>
-      <View style={[pal.borderDark, styles.group]}>
-        <View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
-          <FontAwesomeIcon
-            icon="at"
-            style={[pal.textLight, styles.groupContentIcon]}
-          />
-          <TextInput
-            testID="loginUsernameInput"
-            style={[pal.text, styles.textInput]}
-            placeholder={_(msg`Username or email address`)}
-            placeholderTextColor={pal.colors.textLight}
-            autoCapitalize="none"
-            autoFocus
-            autoCorrect={false}
-            autoComplete="username"
-            returnKeyType="next"
-            textContentType="username"
-            onSubmitEditing={() => {
-              passwordInputRef.current?.focus()
-            }}
-            blurOnSubmit={false} // prevents flickering due to onSubmitEditing going to next field
-            keyboardAppearance={theme.colorScheme}
-            value={identifier}
-            onChangeText={str =>
-              setIdentifier((str || '').toLowerCase().trim())
-            }
-            editable={!isProcessing}
-            accessibilityLabel={_(msg`Username or email address`)}
-            accessibilityHint={_(
-              msg`Input the username or email address you used at signup`,
-            )}
-          />
-        </View>
-        <View style={[pal.borderDark, styles.groupContent]}>
-          <FontAwesomeIcon
-            icon="lock"
-            style={[pal.textLight, styles.groupContentIcon]}
-          />
-          <TextInput
-            testID="loginPasswordInput"
-            ref={passwordInputRef}
-            style={[pal.text, styles.textInput]}
-            placeholder="Password"
-            placeholderTextColor={pal.colors.textLight}
-            autoCapitalize="none"
-            autoCorrect={false}
-            autoComplete="password"
-            returnKeyType="done"
-            enablesReturnKeyAutomatically={true}
-            keyboardAppearance={theme.colorScheme}
-            secureTextEntry={true}
-            textContentType="password"
-            clearButtonMode="while-editing"
-            value={password}
-            onChangeText={setPassword}
-            onSubmitEditing={onPressNext}
-            blurOnSubmit={false} // HACK: https://github.com/facebook/react-native/issues/21911#issuecomment-558343069 Keyboard blur behavior is now handled in onSubmitEditing
-            editable={!isProcessing}
-            accessibilityLabel={_(msg`Password`)}
-            accessibilityHint={
-              identifier === ''
-                ? _(msg`Input your password`)
-                : _(msg`Input the password tied to ${identifier}`)
-            }
-          />
-          <TouchableOpacity
-            testID="forgotPasswordButton"
-            style={styles.textInputInnerBtn}
-            onPress={onPressForgotPassword}
-            accessibilityRole="button"
-            accessibilityLabel={_(msg`Forgot password`)}
-            accessibilityHint={_(msg`Opens password reset form`)}>
-            <Text style={pal.link}>
-              <Trans>Forgot</Trans>
-            </Text>
-          </TouchableOpacity>
-        </View>
-      </View>
-      {error ? (
-        <View style={styles.error}>
-          <View style={styles.errorIcon}>
-            <FontAwesomeIcon icon="exclamation" style={s.white} size={10} />
-          </View>
-          <View style={s.flex1}>
-            <Text style={[s.white, s.bold]}>{error}</Text>
-          </View>
-        </View>
-      ) : undefined}
-      <View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
-        <TouchableOpacity onPress={onPressBack} accessibilityRole="button">
-          <Text type="xl" style={[pal.link, s.pl5]}>
-            <Trans>Back</Trans>
-          </Text>
-        </TouchableOpacity>
-        <View style={s.flex1} />
-        {!serviceDescription && error ? (
-          <TouchableOpacity
-            testID="loginRetryButton"
-            onPress={onPressRetryConnect}
-            accessibilityRole="button"
-            accessibilityLabel={_(msg`Retry`)}
-            accessibilityHint={_(msg`Retries login`)}>
-            <Text type="xl-bold" style={[pal.link, s.pr5]}>
-              <Trans>Retry</Trans>
-            </Text>
-          </TouchableOpacity>
-        ) : !serviceDescription ? (
-          <>
-            <ActivityIndicator />
-            <Text type="xl" style={[pal.textLight, s.pl10]}>
-              <Trans>Connecting...</Trans>
-            </Text>
-          </>
-        ) : isProcessing ? (
-          <ActivityIndicator />
-        ) : isReady ? (
-          <TouchableOpacity
-            testID="loginNextButton"
-            onPress={onPressNext}
-            accessibilityRole="button"
-            accessibilityLabel={_(msg`Go to next`)}
-            accessibilityHint={_(msg`Navigates to the next screen`)}>
-            <Text type="xl-bold" style={[pal.link, s.pr5]}>
-              <Trans>Next</Trans>
-            </Text>
-          </TouchableOpacity>
-        ) : undefined}
-      </View>
-    </View>
-  )
-}
diff --git a/src/view/com/auth/login/PasswordUpdatedForm.tsx b/src/view/com/auth/login/PasswordUpdatedForm.tsx
deleted file mode 100644
index 71f750b14..000000000
--- a/src/view/com/auth/login/PasswordUpdatedForm.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-import React, {useEffect} from 'react'
-import {TouchableOpacity, View} from 'react-native'
-import {useAnalytics} from 'lib/analytics/analytics'
-import {Text} from '../../util/text/Text'
-import {s} from 'lib/styles'
-import {usePalette} from 'lib/hooks/usePalette'
-import {styles} from './styles'
-import {msg, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-
-export const PasswordUpdatedForm = ({
-  onPressNext,
-}: {
-  onPressNext: () => void
-}) => {
-  const {screen} = useAnalytics()
-  const pal = usePalette('default')
-  const {_} = useLingui()
-
-  useEffect(() => {
-    screen('Signin:PasswordUpdatedForm')
-  }, [screen])
-
-  return (
-    <>
-      <View>
-        <Text type="title-lg" style={[pal.text, styles.screenTitle]}>
-          <Trans>Password updated!</Trans>
-        </Text>
-        <Text type="lg" style={[pal.text, styles.instructions]}>
-          <Trans>You can now sign in with your new password.</Trans>
-        </Text>
-        <View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
-          <View style={s.flex1} />
-          <TouchableOpacity
-            onPress={onPressNext}
-            accessibilityRole="button"
-            accessibilityLabel={_(msg`Close alert`)}
-            accessibilityHint={_(msg`Closes password update alert`)}>
-            <Text type="xl-bold" style={[pal.link, s.pr5]}>
-              <Trans>Okay</Trans>
-            </Text>
-          </TouchableOpacity>
-        </View>
-      </View>
-    </>
-  )
-}
diff --git a/src/view/com/auth/login/SetNewPasswordForm.tsx b/src/view/com/auth/login/SetNewPasswordForm.tsx
deleted file mode 100644
index 6d1584c86..000000000
--- a/src/view/com/auth/login/SetNewPasswordForm.tsx
+++ /dev/null
@@ -1,211 +0,0 @@
-import React, {useState, useEffect} from 'react'
-import {
-  ActivityIndicator,
-  TextInput,
-  TouchableOpacity,
-  View,
-} from 'react-native'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {BskyAgent} from '@atproto/api'
-import {useAnalytics} from 'lib/analytics/analytics'
-import {Text} from '../../util/text/Text'
-import {s} from 'lib/styles'
-import {isNetworkError} from 'lib/strings/errors'
-import {usePalette} from 'lib/hooks/usePalette'
-import {useTheme} from 'lib/ThemeContext'
-import {cleanError} from 'lib/strings/errors'
-import {checkAndFormatResetCode} from 'lib/strings/password'
-import {logger} from '#/logger'
-import {styles} from './styles'
-import {Trans, msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-
-export const SetNewPasswordForm = ({
-  error,
-  serviceUrl,
-  setError,
-  onPressBack,
-  onPasswordSet,
-}: {
-  error: string
-  serviceUrl: string
-  setError: (v: string) => void
-  onPressBack: () => void
-  onPasswordSet: () => void
-}) => {
-  const pal = usePalette('default')
-  const theme = useTheme()
-  const {screen} = useAnalytics()
-  const {_} = useLingui()
-
-  useEffect(() => {
-    screen('Signin:SetNewPasswordForm')
-  }, [screen])
-
-  const [isProcessing, setIsProcessing] = useState<boolean>(false)
-  const [resetCode, setResetCode] = useState<string>('')
-  const [password, setPassword] = useState<string>('')
-
-  const onPressNext = async () => {
-    // Check that the code is correct. We do this again just incase the user enters the code after their pw and we
-    // don't get to call onBlur first
-    const formattedCode = checkAndFormatResetCode(resetCode)
-    // TODO Better password strength check
-    if (!formattedCode || !password) {
-      setError(
-        _(
-          msg`You have entered an invalid code. It should look like XXXXX-XXXXX.`,
-        ),
-      )
-      return
-    }
-
-    setError('')
-    setIsProcessing(true)
-
-    try {
-      const agent = new BskyAgent({service: serviceUrl})
-      await agent.com.atproto.server.resetPassword({
-        token: formattedCode,
-        password,
-      })
-      onPasswordSet()
-    } catch (e: any) {
-      const errMsg = e.toString()
-      logger.warn('Failed to set new password', {error: e})
-      setIsProcessing(false)
-      if (isNetworkError(e)) {
-        setError(
-          'Unable to contact your service. Please check your Internet connection.',
-        )
-      } else {
-        setError(cleanError(errMsg))
-      }
-    }
-  }
-
-  const onBlur = () => {
-    const formattedCode = checkAndFormatResetCode(resetCode)
-    if (!formattedCode) {
-      setError(
-        _(
-          msg`You have entered an invalid code. It should look like XXXXX-XXXXX.`,
-        ),
-      )
-      return
-    }
-    setResetCode(formattedCode)
-  }
-
-  return (
-    <>
-      <View>
-        <Text type="title-lg" style={[pal.text, styles.screenTitle]}>
-          <Trans>Set new password</Trans>
-        </Text>
-        <Text type="lg" style={[pal.text, styles.instructions]}>
-          <Trans>
-            You will receive an email with a "reset code." Enter that code here,
-            then enter your new password.
-          </Trans>
-        </Text>
-        <View
-          testID="newPasswordView"
-          style={[pal.view, pal.borderDark, styles.group]}>
-          <View
-            style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
-            <FontAwesomeIcon
-              icon="ticket"
-              style={[pal.textLight, styles.groupContentIcon]}
-            />
-            <TextInput
-              testID="resetCodeInput"
-              style={[pal.text, styles.textInput]}
-              placeholder={_(msg`Reset code`)}
-              placeholderTextColor={pal.colors.textLight}
-              autoCapitalize="none"
-              autoCorrect={false}
-              keyboardAppearance={theme.colorScheme}
-              autoComplete="off"
-              value={resetCode}
-              onChangeText={setResetCode}
-              onFocus={() => setError('')}
-              onBlur={onBlur}
-              editable={!isProcessing}
-              accessible={true}
-              accessibilityLabel={_(msg`Reset code`)}
-              accessibilityHint={_(
-                msg`Input code sent to your email for password reset`,
-              )}
-            />
-          </View>
-          <View style={[pal.borderDark, styles.groupContent]}>
-            <FontAwesomeIcon
-              icon="lock"
-              style={[pal.textLight, styles.groupContentIcon]}
-            />
-            <TextInput
-              testID="newPasswordInput"
-              style={[pal.text, styles.textInput]}
-              placeholder={_(msg`New password`)}
-              placeholderTextColor={pal.colors.textLight}
-              autoCapitalize="none"
-              autoCorrect={false}
-              autoComplete="new-password"
-              keyboardAppearance={theme.colorScheme}
-              secureTextEntry
-              value={password}
-              onChangeText={setPassword}
-              editable={!isProcessing}
-              accessible={true}
-              accessibilityLabel={_(msg`Password`)}
-              accessibilityHint={_(msg`Input new password`)}
-            />
-          </View>
-        </View>
-        {error ? (
-          <View style={styles.error}>
-            <View style={styles.errorIcon}>
-              <FontAwesomeIcon icon="exclamation" style={s.white} size={10} />
-            </View>
-            <View style={s.flex1}>
-              <Text style={[s.white, s.bold]}>{error}</Text>
-            </View>
-          </View>
-        ) : undefined}
-        <View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
-          <TouchableOpacity onPress={onPressBack} accessibilityRole="button">
-            <Text type="xl" style={[pal.link, s.pl5]}>
-              <Trans>Back</Trans>
-            </Text>
-          </TouchableOpacity>
-          <View style={s.flex1} />
-          {isProcessing ? (
-            <ActivityIndicator />
-          ) : !resetCode || !password ? (
-            <Text type="xl-bold" style={[pal.link, s.pr5, styles.dimmed]}>
-              <Trans>Next</Trans>
-            </Text>
-          ) : (
-            <TouchableOpacity
-              testID="setNewPasswordButton"
-              // Check the code before running the callback
-              onPress={onPressNext}
-              accessibilityRole="button"
-              accessibilityLabel={_(msg`Go to next`)}
-              accessibilityHint={_(msg`Navigates to the next screen`)}>
-              <Text type="xl-bold" style={[pal.link, s.pr5]}>
-                <Trans>Next</Trans>
-              </Text>
-            </TouchableOpacity>
-          )}
-          {isProcessing ? (
-            <Text type="xl" style={[pal.textLight, s.pl10]}>
-              <Trans>Updating...</Trans>
-            </Text>
-          ) : undefined}
-        </View>
-      </View>
-    </>
-  )
-}
diff --git a/src/view/com/auth/login/styles.ts b/src/view/com/auth/login/styles.ts
deleted file mode 100644
index 9dccc2803..000000000
--- a/src/view/com/auth/login/styles.ts
+++ /dev/null
@@ -1,118 +0,0 @@
-import {StyleSheet} from 'react-native'
-import {colors} from 'lib/styles'
-import {isWeb} from '#/platform/detection'
-
-export const styles = StyleSheet.create({
-  screenTitle: {
-    marginBottom: 10,
-    marginHorizontal: 20,
-  },
-  instructions: {
-    marginBottom: 20,
-    marginHorizontal: 20,
-  },
-  group: {
-    borderWidth: 1,
-    borderRadius: 10,
-    marginBottom: 20,
-    marginHorizontal: 20,
-  },
-  groupLabel: {
-    paddingHorizontal: 20,
-    paddingBottom: 5,
-  },
-  groupContent: {
-    borderTopWidth: 1,
-    flexDirection: 'row',
-    alignItems: 'center',
-  },
-  noTopBorder: {
-    borderTopWidth: 0,
-  },
-  groupContentIcon: {
-    marginLeft: 10,
-  },
-  account: {
-    borderTopWidth: 1,
-    paddingHorizontal: 20,
-    paddingVertical: 4,
-  },
-  accountLast: {
-    borderBottomWidth: 1,
-    marginBottom: 20,
-    paddingVertical: 8,
-  },
-  textInput: {
-    flex: 1,
-    width: '100%',
-    paddingVertical: 10,
-    paddingHorizontal: 12,
-    fontSize: 17,
-    letterSpacing: 0.25,
-    fontWeight: '400',
-    borderRadius: 10,
-  },
-  textInputInnerBtn: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    paddingVertical: 6,
-    paddingHorizontal: 8,
-    marginHorizontal: 6,
-  },
-  textBtn: {
-    flexDirection: 'row',
-    flex: 1,
-    alignItems: 'center',
-  },
-  textBtnLabel: {
-    flex: 1,
-    paddingVertical: 10,
-    paddingHorizontal: 12,
-  },
-  textBtnFakeInnerBtn: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    borderRadius: 6,
-    paddingVertical: 6,
-    paddingHorizontal: 8,
-    marginHorizontal: 6,
-  },
-  accountText: {
-    flex: 1,
-    flexDirection: 'row',
-    alignItems: 'baseline',
-    paddingVertical: 10,
-  },
-  accountTextOther: {
-    paddingLeft: 12,
-  },
-  error: {
-    backgroundColor: colors.red4,
-    flexDirection: 'row',
-    alignItems: 'center',
-    marginTop: -5,
-    marginHorizontal: 20,
-    marginBottom: 15,
-    borderRadius: 8,
-    paddingHorizontal: 8,
-    paddingVertical: 8,
-  },
-  errorIcon: {
-    borderWidth: 1,
-    borderColor: colors.white,
-    color: colors.white,
-    borderRadius: 30,
-    width: 16,
-    height: 16,
-    alignItems: 'center',
-    justifyContent: 'center',
-    marginRight: 5,
-  },
-  dimmed: {opacity: 0.5},
-
-  maxHeight: {
-    // @ts-ignore web only -prf
-    maxHeight: isWeb ? '100vh' : undefined,
-    height: !isWeb ? '100%' : undefined,
-  },
-})
diff --git a/src/view/com/auth/onboarding/RecommendedFollowsItem.tsx b/src/view/com/auth/onboarding/RecommendedFollowsItem.tsx
index 07001068c..dba3f8c56 100644
--- a/src/view/com/auth/onboarding/RecommendedFollowsItem.tsx
+++ b/src/view/com/auth/onboarding/RecommendedFollowsItem.tsx
@@ -1,6 +1,6 @@
 import React from 'react'
 import {View, StyleSheet, ActivityIndicator} from 'react-native'
-import {ProfileModeration, AppBskyActorDefs} from '@atproto/api'
+import {ModerationDecision, AppBskyActorDefs} from '@atproto/api'
 import {Button} from '#/view/com/util/forms/Button'
 import {usePalette} from 'lib/hooks/usePalette'
 import {sanitizeDisplayName} from 'lib/strings/display-names'
@@ -11,14 +11,15 @@ import {Text} from 'view/com/util/text/Text'
 import Animated, {FadeInRight} from 'react-native-reanimated'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {useAnalytics} from 'lib/analytics/analytics'
-import {Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {Trans, msg} from '@lingui/macro'
 import {Shadow, useProfileShadow} from '#/state/cache/profile-shadow'
 import {useProfileFollowMutationQueue} from '#/state/queries/profile'
 import {logger} from '#/logger'
 
 type Props = {
   profile: AppBskyActorDefs.ProfileViewBasic
-  moderation: ProfileModeration
+  moderation: ModerationDecision
   onFollowStateChange: (props: {
     did: string
     following: boolean
@@ -56,13 +57,13 @@ export function RecommendedFollowsItem({
   )
 }
 
-export function ProfileCard({
+function ProfileCard({
   profile,
   onFollowStateChange,
   moderation,
 }: {
   profile: Shadow<AppBskyActorDefs.ProfileViewBasic>
-  moderation: ProfileModeration
+  moderation: ModerationDecision
   onFollowStateChange: (props: {
     did: string
     following: boolean
@@ -70,9 +71,13 @@ export function ProfileCard({
 }) {
   const {track} = useAnalytics()
   const pal = usePalette('default')
+  const {_} = useLingui()
   const [addingMoreSuggestions, setAddingMoreSuggestions] =
     React.useState(false)
-  const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(profile)
+  const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(
+    profile,
+    'RecommendedFollowsItem',
+  )
 
   const onToggleFollow = React.useCallback(async () => {
     try {
@@ -110,7 +115,7 @@ export function ProfileCard({
           <UserAvatar
             size={40}
             avatar={profile.avatar}
-            moderation={moderation.avatar}
+            moderation={moderation.ui('avatar')}
           />
         </View>
         <View style={styles.layoutContent}>
@@ -121,7 +126,7 @@ export function ProfileCard({
             lineHeight={1.2}>
             {sanitizeDisplayName(
               profile.displayName || sanitizeHandle(profile.handle),
-              moderation.profile,
+              moderation.ui('displayName'),
             )}
           </Text>
           <Text type="xl" style={[pal.textLight]} numberOfLines={1}>
@@ -133,7 +138,7 @@ export function ProfileCard({
           type={profile.viewer?.following ? 'default' : 'inverted'}
           labelStyle={styles.followButton}
           onPress={onToggleFollow}
-          label={profile.viewer?.following ? 'Unfollow' : 'Follow'}
+          label={profile.viewer?.following ? _(msg`Unfollow`) : _(msg`Follow`)}
         />
       </View>
       {profile.description ? (
diff --git a/src/view/com/auth/onboarding/WelcomeMobile.tsx b/src/view/com/auth/onboarding/WelcomeMobile.tsx
index 5de1a7817..b8659d56c 100644
--- a/src/view/com/auth/onboarding/WelcomeMobile.tsx
+++ b/src/view/com/auth/onboarding/WelcomeMobile.tsx
@@ -6,7 +6,8 @@ import {usePalette} from 'lib/hooks/usePalette'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {Button} from 'view/com/util/forms/Button'
 import {ViewHeader} from 'view/com/util/ViewHeader'
-import {Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {Trans, msg} from '@lingui/macro'
 
 type Props = {
   next: () => void
@@ -15,6 +16,7 @@ type Props = {
 
 export function WelcomeMobile({next, skip}: Props) {
   const pal = usePalette('default')
+  const {_} = useLingui()
 
   return (
     <View style={[styles.container]} testID="welcomeOnboarding">
@@ -91,7 +93,7 @@ export function WelcomeMobile({next, skip}: Props) {
 
       <Button
         onPress={next}
-        label="Continue"
+        label={_(msg`Continue`)}
         testID="continueBtn"
         style={[styles.buttonContainer]}
         labelStyle={styles.buttonText}
diff --git a/src/view/com/auth/server-input/index.tsx b/src/view/com/auth/server-input/index.tsx
index a70621973..b26ac1dcb 100644
--- a/src/view/com/auth/server-input/index.tsx
+++ b/src/view/com/auth/server-input/index.tsx
@@ -2,7 +2,7 @@ import React from 'react'
 import {View} from 'react-native'
 import {useLingui} from '@lingui/react'
 import {Trans, msg} from '@lingui/macro'
-import {PROD_SERVICE} from 'lib/constants'
+import {BSKY_SERVICE} from 'lib/constants'
 import * as persisted from '#/state/persisted'
 
 import {atoms as a, useBreakpoints, useTheme} from '#/alf'
@@ -26,7 +26,7 @@ export function ServerInputDialog({
   const [pdsAddressHistory, setPdsAddressHistory] = React.useState<string[]>(
     persisted.get('pdsAddressHistory') || [],
   )
-  const [fixedOption, setFixedOption] = React.useState([PROD_SERVICE])
+  const [fixedOption, setFixedOption] = React.useState([BSKY_SERVICE])
   const [customAddress, setCustomAddress] = React.useState('')
 
   const onClose = React.useCallback(() => {
@@ -86,7 +86,7 @@ export function ServerInputDialog({
             label="Preferences"
             values={fixedOption}
             onChange={setFixedOption}>
-            <ToggleButton.Button name={PROD_SERVICE} label={_(msg`Bluesky`)}>
+            <ToggleButton.Button name={BSKY_SERVICE} label={_(msg`Bluesky`)}>
               {_(msg`Bluesky`)}
             </ToggleButton.Button>
             <ToggleButton.Button
@@ -115,7 +115,7 @@ export function ServerInputDialog({
                   testID="customServerTextInput"
                   value={customAddress}
                   onChangeText={setCustomAddress}
-                  label={_(msg`my-server.com`)}
+                  label="my-server.com"
                   accessibilityLabelledBy="address-input-label"
                   autoCapitalize="none"
                   keyboardType="url"
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx
index 1ed6b98a5..ac6fe6fed 100644
--- a/src/view/com/composer/Composer.tsx
+++ b/src/view/com/composer/Composer.tsx
@@ -1,5 +1,4 @@
 import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'
-import {observer} from 'mobx-react-lite'
 import {
   ActivityIndicator,
   BackHandler,
@@ -12,57 +11,62 @@ import {
   TouchableOpacity,
   View,
 } from 'react-native'
-import {useSafeAreaInsets} from 'react-native-safe-area-context'
 import LinearGradient from 'react-native-linear-gradient'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {useSafeAreaInsets} from 'react-native-safe-area-context'
 import {RichText} from '@atproto/api'
-import {useAnalytics} from 'lib/analytics/analytics'
-import {useIsKeyboardVisible} from 'lib/hooks/useIsKeyboardVisible'
-import {ExternalEmbed} from './ExternalEmbed'
-import {Text} from '../util/text/Text'
-import * as Toast from '../util/Toast'
-// TODO: Prevent naming components that coincide with RN primitives
-// due to linting false positives
-import {TextInput, TextInputRef} from './text-input/TextInput'
-import {CharProgress} from './char-progress/CharProgress'
-import {UserAvatar} from '../util/UserAvatar'
-import * as apilib from 'lib/api/index'
-import {ComposerOpts} from 'state/shell/composer'
-import {s, colors, gradients} from 'lib/styles'
-import {cleanError} from 'lib/strings/errors'
-import {shortenLinks} from 'lib/strings/rich-text-manip'
-import {toShortUrl} from 'lib/strings/url-helpers'
-import {SelectPhotoBtn} from './photos/SelectPhotoBtn'
-import {OpenCameraBtn} from './photos/OpenCameraBtn'
-import {ThreadgateBtn} from './threadgate/ThreadgateBtn'
-import {usePalette} from 'lib/hooks/usePalette'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {useExternalLinkFetch} from './useExternalLinkFetch'
-import {isWeb, isNative, isAndroid, isIOS} from 'platform/detection'
-import QuoteEmbed from '../util/post-embeds/QuoteEmbed'
-import {GalleryModel} from 'state/models/media/gallery'
-import {Gallery} from './photos/Gallery'
-import {MAX_GRAPHEME_LENGTH} from 'lib/constants'
-import {LabelsBtn} from './labels/LabelsBtn'
-import {SelectLangBtn} from './select-language/SelectLangBtn'
-import {SuggestedLanguage} from './select-language/SuggestedLanguage'
-import {insertMentionAt} from 'lib/strings/mention-manip'
-import {Trans, msg} from '@lingui/macro'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
-import {useModals, useModalControls} from '#/state/modals'
+import {observer} from 'mobx-react-lite'
+
+import {logEvent} from '#/lib/statsig/statsig'
+import {logger} from '#/logger'
+import {emitPostCreated} from '#/state/events'
+import {useModals} from '#/state/modals'
 import {useRequireAltTextEnabled} from '#/state/preferences'
 import {
+  toPostLanguages,
   useLanguagePrefs,
   useLanguagePrefsApi,
-  toPostLanguages,
 } from '#/state/preferences/languages'
-import {useSession, getAgent} from '#/state/session'
 import {useProfileQuery} from '#/state/queries/profile'
-import {useComposerControls} from '#/state/shell/composer'
-import {emitPostCreated} from '#/state/events'
 import {ThreadgateSetting} from '#/state/queries/threadgate'
-import {logger} from '#/logger'
+import {getAgent, useSession} from '#/state/session'
+import {useComposerControls} from '#/state/shell/composer'
+import {useAnalytics} from 'lib/analytics/analytics'
+import * as apilib from 'lib/api/index'
+import {MAX_GRAPHEME_LENGTH} from 'lib/constants'
+import {useIsKeyboardVisible} from 'lib/hooks/useIsKeyboardVisible'
+import {usePalette} from 'lib/hooks/usePalette'
+import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
+import {cleanError} from 'lib/strings/errors'
+import {insertMentionAt} from 'lib/strings/mention-manip'
+import {shortenLinks} from 'lib/strings/rich-text-manip'
+import {toShortUrl} from 'lib/strings/url-helpers'
+import {colors, gradients, s} from 'lib/styles'
+import {isAndroid, isIOS, isNative, isWeb} from 'platform/detection'
+import {useDialogStateControlContext} from 'state/dialogs'
+import {GalleryModel} from 'state/models/media/gallery'
+import {ComposerOpts} from 'state/shell/composer'
 import {ComposerReplyTo} from 'view/com/composer/ComposerReplyTo'
+import * as Prompt from '#/components/Prompt'
+import {QuoteEmbed} from '../util/post-embeds/QuoteEmbed'
+import {Text} from '../util/text/Text'
+import * as Toast from '../util/Toast'
+import {UserAvatar} from '../util/UserAvatar'
+import {CharProgress} from './char-progress/CharProgress'
+import {ExternalEmbed} from './ExternalEmbed'
+import {LabelsBtn} from './labels/LabelsBtn'
+import {Gallery} from './photos/Gallery'
+import {OpenCameraBtn} from './photos/OpenCameraBtn'
+import {SelectPhotoBtn} from './photos/SelectPhotoBtn'
+import {SelectLangBtn} from './select-language/SelectLangBtn'
+import {SuggestedLanguage} from './select-language/SuggestedLanguage'
+// TODO: Prevent naming components that coincide with RN primitives
+// due to linting false positives
+import {TextInput, TextInputRef} from './text-input/TextInput'
+import {ThreadgateBtn} from './threadgate/ThreadgateBtn'
+import {useExternalLinkFetch} from './useExternalLinkFetch'
 
 type Props = ComposerOpts
 export const ComposePost = observer(function ComposePost({
@@ -71,11 +75,12 @@ export const ComposePost = observer(function ComposePost({
   quote: initQuote,
   mention: initMention,
   openPicker,
+  text: initText,
+  imageUris: initImageUris,
 }: Props) {
   const {currentAccount} = useSession()
   const {data: currentProfile} = useProfileQuery({did: currentAccount!.did})
-  const {isModalActive, activeModals} = useModals()
-  const {openModal, closeModal} = useModalControls()
+  const {isModalActive} = useModals()
   const {closeComposer} = useComposerControls()
   const {track} = useAnalytics()
   const pal = usePalette('default')
@@ -85,13 +90,18 @@ export const ComposePost = observer(function ComposePost({
   const langPrefs = useLanguagePrefs()
   const setLangPrefs = useLanguagePrefsApi()
   const textInput = useRef<TextInputRef>(null)
+  const discardPromptControl = Prompt.usePromptControl()
+  const {closeAllDialogs} = useDialogStateControlContext()
+
   const [isKeyboardVisible] = useIsKeyboardVisible({iosUseWillEvents: true})
   const [isProcessing, setIsProcessing] = useState(false)
   const [processingState, setProcessingState] = useState('')
   const [error, setError] = useState('')
   const [richtext, setRichText] = useState(
     new RichText({
-      text: initMention
+      text: initText
+        ? initText
+        : initMention
         ? insertMentionAt(
             `@${initMention}`,
             initMention.length + 1,
@@ -110,7 +120,10 @@ export const ComposePost = observer(function ComposePost({
   const [labels, setLabels] = useState<string[]>([])
   const [threadgate, setThreadgate] = useState<ThreadgateSetting[]>([])
   const [suggestedLinks, setSuggestedLinks] = useState<Set<string>>(new Set())
-  const gallery = useMemo(() => new GalleryModel(), [])
+  const gallery = useMemo(
+    () => new GalleryModel(initImageUris),
+    [initImageUris],
+  )
   const onClose = useCallback(() => {
     closeComposer()
   }, [closeComposer])
@@ -127,27 +140,21 @@ export const ComposePost = observer(function ComposePost({
 
   const onPressCancel = useCallback(() => {
     if (graphemeLength > 0 || !gallery.isEmpty) {
-      if (activeModals.some(modal => modal.name === 'confirm')) {
-        closeModal()
-      }
+      closeAllDialogs()
       if (Keyboard) {
         Keyboard.dismiss()
       }
-      openModal({
-        name: 'confirm',
-        title: _(msg`Discard draft`),
-        onPressConfirm: onClose,
-        onPressCancel: () => {
-          closeModal()
-        },
-        message: _(msg`Are you sure you'd like to discard this draft?`),
-        confirmBtnText: _(msg`Discard`),
-        confirmBtnStyle: {backgroundColor: colors.red4},
-      })
+      discardPromptControl.open()
     } else {
       onClose()
     }
-  }, [openModal, closeModal, activeModals, onClose, graphemeLength, gallery, _])
+  }, [
+    graphemeLength,
+    gallery.isEmpty,
+    closeAllDialogs,
+    discardPromptControl,
+    onClose,
+  ])
   // android back button
   useEffect(() => {
     if (!isAndroid) {
@@ -250,6 +257,16 @@ export const ComposePost = observer(function ComposePost({
       setIsProcessing(false)
       return
     } finally {
+      if (postUri) {
+        logEvent('post:create', {
+          imageCount: gallery.size,
+          isReply: replyTo != null,
+          hasLink: extLink != null,
+          hasQuote: quote != null,
+          langs: langPrefs.postLanguage,
+          logContext: 'Composer',
+        })
+      }
       track('Create Post', {
         imageCount: gallery.size,
       })
@@ -399,7 +416,11 @@ export const ComposePost = observer(function ComposePost({
               styles.textInputLayout,
               isNative && styles.textInputLayoutMobile,
             ]}>
-            <UserAvatar avatar={currentProfile?.avatar} size={50} />
+            <UserAvatar
+              avatar={currentProfile?.avatar}
+              size={50}
+              type={currentProfile?.associated?.labeler ? 'labeler' : 'user'}
+            />
             <TextInput
               ref={textInput}
               richtext={richtext}
@@ -427,7 +448,7 @@ export const ComposePost = observer(function ComposePost({
             />
           )}
           {quote ? (
-            <View style={[s.mt5, isWeb && s.mb10]}>
+            <View style={[s.mt5, isWeb && s.mb10, {pointerEvents: 'none'}]}>
               <QuoteEmbed quote={quote} />
             </View>
           ) : undefined}
@@ -481,6 +502,21 @@ export const ComposePost = observer(function ComposePost({
           <CharProgress count={graphemeLength} />
         </View>
       </View>
+
+      <Prompt.Basic
+        control={discardPromptControl}
+        title={_(msg`Discard draft?`)}
+        description={_(msg`Are you sure you'd like to discard this draft?`)}
+        onConfirm={() => {
+          if (isWeb) {
+            onClose()
+          } else {
+            discardPromptControl.close(onClose)
+          }
+        }}
+        confirmButtonCta={_(msg`Discard`)}
+        confirmButtonColor="negative"
+      />
     </KeyboardAvoidingView>
   )
 })
diff --git a/src/view/com/composer/ComposerReplyTo.tsx b/src/view/com/composer/ComposerReplyTo.tsx
index 39a1473a3..0c1b87d04 100644
--- a/src/view/com/composer/ComposerReplyTo.tsx
+++ b/src/view/com/composer/ComposerReplyTo.tsx
@@ -15,7 +15,7 @@ import {sanitizeDisplayName} from 'lib/strings/display-names'
 import {sanitizeHandle} from 'lib/strings/handles'
 import {UserAvatar} from 'view/com/util/UserAvatar'
 import {Text} from 'view/com/util/text/Text'
-import QuoteEmbed from 'view/com/util/post-embeds/QuoteEmbed'
+import {QuoteEmbed} from 'view/com/util/post-embeds/QuoteEmbed'
 
 export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) {
   const pal = usePalette('default')
@@ -86,7 +86,8 @@ export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) {
       <UserAvatar
         avatar={replyTo.author.avatar}
         size={50}
-        moderation={replyTo.moderation?.avatar}
+        moderation={replyTo.moderation?.ui('avatar')}
+        type={replyTo.author.associated?.labeler ? 'labeler' : 'user'}
       />
       <View style={styles.replyToPost}>
         <Text type="xl-medium" style={[pal.text]}>
@@ -103,7 +104,7 @@ export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) {
               {replyTo.text}
             </Text>
           </View>
-          {images && !replyTo.moderation?.embed.blur && (
+          {images && !replyTo.moderation?.ui('contentMedia').blur && (
             <ComposerReplyToImages images={images} showFull={showFull} />
           )}
         </View>
diff --git a/src/view/com/composer/Prompt.tsx b/src/view/com/composer/Prompt.tsx
index 632bb2634..16d1b6fb9 100644
--- a/src/view/com/composer/Prompt.tsx
+++ b/src/view/com/composer/Prompt.tsx
@@ -23,7 +23,11 @@ export function ComposePrompt({onPressCompose}: {onPressCompose: () => void}) {
       accessibilityRole="button"
       accessibilityLabel={_(msg`Compose reply`)}
       accessibilityHint={_(msg`Opens composer`)}>
-      <UserAvatar avatar={profile?.avatar} size={38} />
+      <UserAvatar
+        avatar={profile?.avatar}
+        size={38}
+        type={profile?.associated?.labeler ? 'labeler' : 'user'}
+      />
       <Text
         type="xl"
         style={[
diff --git a/src/view/com/composer/photos/OpenCameraBtn.tsx b/src/view/com/composer/photos/OpenCameraBtn.tsx
index a288e7310..4353704d5 100644
--- a/src/view/com/composer/photos/OpenCameraBtn.tsx
+++ b/src/view/com/composer/photos/OpenCameraBtn.tsx
@@ -1,5 +1,6 @@
 import React, {useCallback} from 'react'
 import {TouchableOpacity, StyleSheet} from 'react-native'
+import * as MediaLibrary from 'expo-media-library'
 import {
   FontAwesomeIcon,
   FontAwesomeIconStyle,
@@ -24,6 +25,8 @@ export function OpenCameraBtn({gallery}: Props) {
   const {track} = useAnalytics()
   const {_} = useLingui()
   const {requestCameraAccessIfNeeded} = useCameraPermission()
+  const [mediaPermissionRes, requestMediaPermission] =
+    MediaLibrary.usePermissions()
 
   const onPressTakePicture = useCallback(async () => {
     track('Composer:CameraOpened')
@@ -31,6 +34,9 @@ export function OpenCameraBtn({gallery}: Props) {
       if (!(await requestCameraAccessIfNeeded())) {
         return
       }
+      if (!mediaPermissionRes?.granted && mediaPermissionRes?.canAskAgain) {
+        await requestMediaPermission()
+      }
 
       const img = await openCamera({
         width: POST_IMG_MAX.width,
@@ -38,12 +44,23 @@ export function OpenCameraBtn({gallery}: Props) {
         freeStyleCropEnabled: true,
       })
 
+      // If we don't have permissions it's fine, we just wont save it. The post itself will still have access to
+      // the image even without these permissions
+      if (mediaPermissionRes) {
+        await MediaLibrary.createAssetAsync(img.path)
+      }
       gallery.add(img)
     } catch (err: any) {
       // ignore
       logger.warn('Error using camera', {error: err})
     }
-  }, [gallery, track, requestCameraAccessIfNeeded])
+  }, [
+    gallery,
+    track,
+    requestCameraAccessIfNeeded,
+    mediaPermissionRes,
+    requestMediaPermission,
+  ])
 
   const shouldShowCameraButton = isNative || isMobileWeb
   if (!shouldShowCameraButton) {
diff --git a/src/view/com/composer/select-language/SelectLangBtn.tsx b/src/view/com/composer/select-language/SelectLangBtn.tsx
index 78b1e9ba2..785622225 100644
--- a/src/view/com/composer/select-language/SelectLangBtn.tsx
+++ b/src/view/com/composer/select-language/SelectLangBtn.tsx
@@ -20,7 +20,7 @@ import {
   toPostLanguages,
   hasPostLanguage,
 } from '#/state/preferences/languages'
-import {t, msg} from '@lingui/macro'
+import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
 export function SelectLangBtn() {
@@ -84,15 +84,15 @@ export function SelectLangBtn() {
     }
 
     return [
-      {heading: true, label: t`Post language`},
+      {heading: true, label: _(msg`Post language`)},
       ...arr.slice(0, 6),
       {sep: true},
       {
-        label: t`Other...`,
+        label: _(msg`Other...`),
         onPress: onPressMore,
       },
     ]
-  }, [onPressMore, langPrefs, setLangPrefs, postLanguagesPref])
+  }, [onPressMore, langPrefs, setLangPrefs, postLanguagesPref, _])
 
   return (
     <DropdownButton
diff --git a/src/view/com/composer/text-input/TextInput.tsx b/src/view/com/composer/text-input/TextInput.tsx
index 17f9513b7..20be585c2 100644
--- a/src/view/com/composer/text-input/TextInput.tsx
+++ b/src/view/com/composer/text-input/TextInput.tsx
@@ -190,12 +190,11 @@ export const TextInput = forwardRef(function TextInputImpl(
     let i = 0
 
     return Array.from(richtext.segments()).map(segment => {
-      const isTag = AppBskyRichtextFacet.isTag(segment.facet?.features?.[0])
       return (
         <Text
           key={i++}
           style={[
-            segment.facet && !isTag ? pal.link : pal.text,
+            segment.facet ? pal.link : pal.text,
             styles.textInputFormatting,
           ]}>
           {segment.text}
diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx
index 199f1f749..c62d11201 100644
--- a/src/view/com/composer/text-input/TextInput.web.tsx
+++ b/src/view/com/composer/text-input/TextInput.web.tsx
@@ -23,6 +23,7 @@ import {Portal} from '#/components/Portal'
 import {Text} from '../../util/text/Text'
 import {Trans} from '@lingui/macro'
 import Animated, {FadeIn, FadeOut} from 'react-native-reanimated'
+import {TagDecorator} from './web/TagDecorator'
 
 export interface TextInputRef {
   focus: () => void
@@ -67,6 +68,7 @@ export const TextInput = React.forwardRef(function TextInputImpl(
     () => [
       Document,
       LinkDecorator,
+      TagDecorator,
       Mention.configure({
         HTMLAttributes: {
           class: 'mention',
diff --git a/src/view/com/composer/text-input/mobile/Autocomplete.tsx b/src/view/com/composer/text-input/mobile/Autocomplete.tsx
index c400aa48d..9c8f8f916 100644
--- a/src/view/com/composer/text-input/mobile/Autocomplete.tsx
+++ b/src/view/com/composer/text-input/mobile/Autocomplete.tsx
@@ -78,7 +78,11 @@ export function Autocomplete({
                   accessibilityLabel={`Select ${item.handle}`}
                   accessibilityHint="">
                   <View style={styles.avatarAndHandle}>
-                    <UserAvatar avatar={item.avatar ?? null} size={24} />
+                    <UserAvatar
+                      avatar={item.avatar ?? null}
+                      size={24}
+                      type={item.associated?.labeler ? 'labeler' : 'user'}
+                    />
                     <Text type="md-medium" style={pal.text}>
                       {displayName}
                     </Text>
diff --git a/src/view/com/composer/text-input/web/Autocomplete.tsx b/src/view/com/composer/text-input/web/Autocomplete.tsx
index 76058fed3..29b8f0bc6 100644
--- a/src/view/com/composer/text-input/web/Autocomplete.tsx
+++ b/src/view/com/composer/text-input/web/Autocomplete.tsx
@@ -175,7 +175,11 @@ const MentionList = forwardRef<MentionListRef, SuggestionProps>(
                   }}
                   accessibilityRole="button">
                   <View style={styles.avatarAndDisplayName}>
-                    <UserAvatar avatar={item.avatar ?? null} size={26} />
+                    <UserAvatar
+                      avatar={item.avatar ?? null}
+                      size={26}
+                      type={item.associated?.labeler ? 'labeler' : 'user'}
+                    />
                     <Text style={pal.text} numberOfLines={1}>
                       {displayName}
                     </Text>
diff --git a/src/view/com/composer/text-input/web/LinkDecorator.ts b/src/view/com/composer/text-input/web/LinkDecorator.ts
index 19945de08..e36ac80e4 100644
--- a/src/view/com/composer/text-input/web/LinkDecorator.ts
+++ b/src/view/com/composer/text-input/web/LinkDecorator.ts
@@ -18,6 +18,8 @@ import {Mark} from '@tiptap/core'
 import {Plugin, PluginKey} from '@tiptap/pm/state'
 import {Node as ProsemirrorNode} from '@tiptap/pm/model'
 import {Decoration, DecorationSet} from '@tiptap/pm/view'
+import {URL_REGEX} from '@atproto/api'
+
 import {isValidDomain} from 'lib/strings/url-helpers'
 
 export const LinkDecorator = Mark.create({
@@ -78,8 +80,7 @@ function linkDecorator() {
 
 function iterateUris(str: string, cb: (from: number, to: number) => void) {
   let match
-  const re =
-    /(^|\s|\()((https?:\/\/[\S]+)|((?<domain>[a-z][a-z0-9]*(\.[a-z0-9]+)+)[\S]*))/gim
+  const re = URL_REGEX
   while ((match = re.exec(str))) {
     let uri = match[2]
     if (!uri.startsWith('http')) {
diff --git a/src/view/com/composer/text-input/web/TagDecorator.ts b/src/view/com/composer/text-input/web/TagDecorator.ts
new file mode 100644
index 000000000..2bf3184a8
--- /dev/null
+++ b/src/view/com/composer/text-input/web/TagDecorator.ts
@@ -0,0 +1,91 @@
+/**
+ * TipTap is a stateful rich-text editor, which is extremely useful
+ * when you _want_ it to be stateful formatting such as bold and italics.
+ *
+ * However we also use "stateless" behaviors, specifically for URLs
+ * where the text itself drives the formatting.
+ *
+ * This plugin uses a regex to detect URIs and then applies
+ * link decorations (a <span> with the "autolink") class. That avoids
+ * adding any stateful formatting to TipTap's document model.
+ *
+ * We then run the URI detection again when constructing the
+ * RichText object from TipTap's output and merge their features into
+ * the facet-set.
+ */
+
+import {Mark} from '@tiptap/core'
+import {Plugin, PluginKey} from '@tiptap/pm/state'
+import {Node as ProsemirrorNode} from '@tiptap/pm/model'
+import {Decoration, DecorationSet} from '@tiptap/pm/view'
+import {TAG_REGEX, TRAILING_PUNCTUATION_REGEX} from '@atproto/api'
+
+function getDecorations(doc: ProsemirrorNode) {
+  const decorations: Decoration[] = []
+
+  doc.descendants((node, pos) => {
+    if (node.isText && node.text) {
+      const regex = TAG_REGEX
+      const textContent = node.textContent
+
+      let match
+      while ((match = regex.exec(textContent))) {
+        const [matchedString, _, tag] = match
+
+        if (!tag || tag.replace(TRAILING_PUNCTUATION_REGEX, '').length > 64)
+          continue
+
+        const [trailingPunc = ''] = tag.match(TRAILING_PUNCTUATION_REGEX) || []
+        const matchedFrom = match.index + matchedString.indexOf(tag)
+        const matchedTo = matchedFrom + (tag.length - trailingPunc.length)
+
+        /*
+         * The match is exclusive of `#` so we need to adjust the start of the
+         * highlight by -1 to include the `#`
+         */
+        const start = pos + matchedFrom - 1
+        const end = pos + matchedTo
+
+        decorations.push(
+          Decoration.inline(start, end, {
+            class: 'autolink',
+          }),
+        )
+      }
+    }
+  })
+
+  return DecorationSet.create(doc, decorations)
+}
+
+const tagDecoratorPlugin: Plugin = new Plugin({
+  key: new PluginKey('link-decorator'),
+
+  state: {
+    init: (_, {doc}) => getDecorations(doc),
+    apply: (transaction, decorationSet) => {
+      if (transaction.docChanged) {
+        return getDecorations(transaction.doc)
+      }
+      return decorationSet.map(transaction.mapping, transaction.doc)
+    },
+  },
+
+  props: {
+    decorations(state) {
+      return tagDecoratorPlugin.getState(state)
+    },
+  },
+})
+
+export const TagDecorator = Mark.create({
+  name: 'tag-decorator',
+  priority: 1000,
+  keepOnSplit: false,
+  inclusive() {
+    return true
+  },
+  addProseMirrorPlugins() {
+    return [tagDecoratorPlugin]
+  },
+})
diff --git a/src/view/com/composer/useExternalLinkFetch.e2e.ts b/src/view/com/composer/useExternalLinkFetch.e2e.ts
new file mode 100644
index 000000000..ccf619db3
--- /dev/null
+++ b/src/view/com/composer/useExternalLinkFetch.e2e.ts
@@ -0,0 +1,45 @@
+import {useState, useEffect} from 'react'
+import * as apilib from 'lib/api/index'
+import {getLinkMeta} from 'lib/link-meta/link-meta'
+import {ComposerOpts} from 'state/shell/composer'
+import {getAgent} from '#/state/session'
+
+export function useExternalLinkFetch({}: {
+  setQuote: (opts: ComposerOpts['quote']) => void
+}) {
+  const [extLink, setExtLink] = useState<apilib.ExternalEmbedDraft | undefined>(
+    undefined,
+  )
+
+  useEffect(() => {
+    let aborted = false
+    const cleanup = () => {
+      aborted = true
+    }
+    if (!extLink) {
+      return cleanup
+    }
+    if (!extLink.meta) {
+      getLinkMeta(getAgent(), extLink.uri).then(meta => {
+        if (aborted) {
+          return
+        }
+        setExtLink({
+          uri: extLink.uri,
+          isLoading: !!meta.image,
+          meta,
+        })
+      })
+      return cleanup
+    }
+    if (extLink.isLoading) {
+      setExtLink({
+        ...extLink,
+        isLoading: false, // done
+      })
+    }
+    return cleanup
+  }, [extLink])
+
+  return {extLink, setExtLink}
+}
diff --git a/src/view/com/feeds/FeedPage.tsx b/src/view/com/feeds/FeedPage.tsx
index 9595e77e5..2d0736b09 100644
--- a/src/view/com/feeds/FeedPage.tsx
+++ b/src/view/com/feeds/FeedPage.tsx
@@ -1,33 +1,28 @@
 import React from 'react'
-import {
-  FontAwesomeIcon,
-  FontAwesomeIconStyle,
-} from '@fortawesome/react-native-fontawesome'
 import {useNavigation} from '@react-navigation/native'
 import {useAnalytics} from 'lib/analytics/analytics'
 import {useQueryClient} from '@tanstack/react-query'
 import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
 import {MainScrollProvider} from '../util/MainScrollProvider'
-import {usePalette} from 'lib/hooks/usePalette'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {useSetMinimalShellMode} from '#/state/shell'
 import {FeedDescriptor, FeedParams} from '#/state/queries/post-feed'
 import {ComposeIcon2} from 'lib/icons'
-import {colors, s} from 'lib/styles'
+import {s} from 'lib/styles'
 import {View, useWindowDimensions} from 'react-native'
 import {ListMethods} from '../util/List'
 import {Feed} from '../posts/Feed'
-import {TextLink} from '../util/Link'
 import {FAB} from '../util/fab/FAB'
 import {LoadLatestBtn} from '../util/load-latest/LoadLatestBtn'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useSession} from '#/state/session'
 import {useComposerControls} from '#/state/shell/composer'
-import {listenSoftReset, emitSoftReset} from '#/state/events'
+import {listenSoftReset} from '#/state/events'
 import {truncateAndInvalidate} from '#/state/queries/util'
 import {TabState, getTabState, getRootNavigation} from '#/lib/routes/helpers'
 import {isNative} from '#/platform/detection'
+import {logEvent} from '#/lib/statsig/statsig'
 
 const POLL_FREQ = 60e3 // 60sec
 
@@ -46,11 +41,9 @@ export function FeedPage({
   renderEmptyState: () => JSX.Element
   renderEndOfFeed?: () => JSX.Element
 }) {
-  const {isSandbox, hasSession} = useSession()
-  const pal = usePalette('default')
+  const {hasSession} = useSession()
   const {_} = useLingui()
   const navigation = useNavigation()
-  const {isDesktop} = useWebMediaQueries()
   const queryClient = useQueryClient()
   const {openComposer} = useComposerControls()
   const [isScrolledDown, setIsScrolledDown] = React.useState(false)
@@ -76,6 +69,10 @@ export function FeedPage({
       scrollToTop()
       truncateAndInvalidate(queryClient, FEED_RQKEY(feed))
       setHasNew(false)
+      logEvent('feed:refresh', {
+        feedType: feed.split('|')[0],
+        reason: 'soft-reset',
+      })
     }
   }, [navigation, isPageFocused, scrollToTop, queryClient, feed, setHasNew])
 
@@ -97,74 +94,12 @@ export function FeedPage({
     scrollToTop()
     truncateAndInvalidate(queryClient, FEED_RQKEY(feed))
     setHasNew(false)
+    logEvent('feed:refresh', {
+      feedType: feed.split('|')[0],
+      reason: 'load-latest',
+    })
   }, [scrollToTop, feed, queryClient, setHasNew])
 
-  const ListHeaderComponent = React.useCallback(() => {
-    if (isDesktop) {
-      return (
-        <View
-          style={[
-            pal.view,
-            {
-              flexDirection: 'row',
-              alignItems: 'center',
-              justifyContent: 'space-between',
-              paddingHorizontal: 18,
-              paddingVertical: 12,
-            },
-          ]}>
-          <TextLink
-            type="title-lg"
-            href="/"
-            style={[pal.text, {fontWeight: 'bold'}]}
-            text={
-              <>
-                {isSandbox ? 'SANDBOX' : 'Bluesky'}{' '}
-                {hasNew && (
-                  <View
-                    style={{
-                      top: -8,
-                      backgroundColor: colors.blue3,
-                      width: 8,
-                      height: 8,
-                      borderRadius: 4,
-                    }}
-                  />
-                )}
-              </>
-            }
-            onPress={emitSoftReset}
-          />
-          {hasSession && (
-            <TextLink
-              type="title-lg"
-              href="/settings/home-feed"
-              style={{fontWeight: 'bold'}}
-              accessibilityLabel={_(msg`Feed Preferences`)}
-              accessibilityHint=""
-              text={
-                <FontAwesomeIcon
-                  icon="sliders"
-                  style={pal.textLight as FontAwesomeIconStyle}
-                />
-              }
-            />
-          )}
-        </View>
-      )
-    }
-    return <></>
-  }, [
-    isDesktop,
-    pal.view,
-    pal.text,
-    pal.textLight,
-    hasNew,
-    _,
-    isSandbox,
-    hasSession,
-  ])
-
   return (
     <View testID={testID} style={s.h100pct}>
       <MainScrollProvider>
@@ -180,7 +115,6 @@ export function FeedPage({
           onHasNew={setHasNew}
           renderEmptyState={renderEmptyState}
           renderEndOfFeed={renderEndOfFeed}
-          ListHeaderComponent={ListHeaderComponent}
           headerOffset={headerOffset}
         />
       </MainScrollProvider>
@@ -209,21 +143,12 @@ export function FeedPage({
 function useHeaderOffset() {
   const {isDesktop, isTablet} = useWebMediaQueries()
   const {fontScale} = useWindowDimensions()
-  const {hasSession} = useSession()
   if (isDesktop || isTablet) {
     return 0
   }
-  if (hasSession) {
-    const navBarPad = 16
-    const navBarText = 21 * fontScale
-    const tabBarPad = 20 + 3 // nav bar padding + border
-    const tabBarText = 16 * fontScale
-    const magic = 7 * fontScale
-    return navBarPad + navBarText + tabBarPad + tabBarText + magic
-  } else {
-    const navBarPad = 16
-    const navBarText = 21 * fontScale
-    const magic = 4 * fontScale
-    return navBarPad + navBarText + magic
-  }
+  const navBarHeight = 42
+  const tabBarPad = 10 + 10 + 3 // padding + border
+  const normalLineHeight = 1.2
+  const tabBarText = 16 * normalLineHeight * fontScale
+  return navBarHeight + tabBarPad + tabBarText
 }
diff --git a/src/view/com/feeds/FeedSourceCard.tsx b/src/view/com/feeds/FeedSourceCard.tsx
index 0de88b248..9300b4159 100644
--- a/src/view/com/feeds/FeedSourceCard.tsx
+++ b/src/view/com/feeds/FeedSourceCard.tsx
@@ -2,18 +2,15 @@ import React from 'react'
 import {Pressable, StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {Text} from '../util/text/Text'
-import {RichText} from '../util/text/RichText'
+import {RichText} from '#/components/RichText'
 import {usePalette} from 'lib/hooks/usePalette'
 import {s} from 'lib/styles'
 import {UserAvatar} from '../util/UserAvatar'
-import {useNavigation} from '@react-navigation/native'
-import {NavigationProp} from 'lib/routes/types'
 import {pluralize} from 'lib/strings/helpers'
 import {AtUri} from '@atproto/api'
 import * as Toast from 'view/com/util/Toast'
 import {sanitizeHandle} from 'lib/strings/handles'
 import {logger} from '#/logger'
-import {useModalControls} from '#/state/modals'
 import {Trans, msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {
@@ -25,6 +22,9 @@ import {
 } from '#/state/queries/preferences'
 import {useFeedSourceInfoQuery, FeedSourceInfo} from '#/state/queries/feed'
 import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
+import {useTheme} from '#/alf'
+import * as Prompt from '#/components/Prompt'
+import {useNavigationDeduped} from 'lib/hooks/useNavigationDeduped'
 
 export function FeedSourceCard({
   feedUri,
@@ -82,10 +82,11 @@ export function FeedSourceCardLoaded({
   pinOnSave?: boolean
   showMinimalPlaceholder?: boolean
 }) {
+  const t = useTheme()
   const pal = usePalette('default')
   const {_} = useLingui()
-  const navigation = useNavigation<NavigationProp>()
-  const {openModal} = useModalControls()
+  const removePromptControl = Prompt.usePromptControl()
+  const navigation = useNavigationDeduped()
 
   const {isPending: isSavePending, mutateAsync: saveFeed} =
     useSaveFeedMutation()
@@ -95,40 +96,45 @@ export function FeedSourceCardLoaded({
 
   const isSaved = Boolean(preferences?.feeds?.saved?.includes(feed?.uri || ''))
 
+  const onSave = React.useCallback(async () => {
+    if (!feed) return
+
+    try {
+      if (pinOnSave) {
+        await pinFeed({uri: feed.uri})
+      } else {
+        await saveFeed({uri: feed.uri})
+      }
+      Toast.show(_(msg`Added to my feeds`))
+    } catch (e) {
+      Toast.show(_(msg`There was an issue contacting your server`))
+      logger.error('Failed to save feed', {message: e})
+    }
+  }, [_, feed, pinFeed, pinOnSave, saveFeed])
+
+  const onUnsave = React.useCallback(async () => {
+    if (!feed) return
+
+    try {
+      await removeFeed({uri: feed.uri})
+      // await item.unsave()
+      Toast.show(_(msg`Removed from my feeds`))
+    } catch (e) {
+      Toast.show(_(msg`There was an issue contacting your server`))
+      logger.error('Failed to unsave feed', {message: e})
+    }
+  }, [_, feed, removeFeed])
+
   const onToggleSaved = React.useCallback(async () => {
     // Only feeds can be un/saved, lists are handled elsewhere
     if (feed?.type !== 'feed') return
 
     if (isSaved) {
-      openModal({
-        name: 'confirm',
-        title: _(msg`Remove from my feeds`),
-        message: _(msg`Remove ${feed?.displayName} from my feeds?`),
-        onPressConfirm: async () => {
-          try {
-            await removeFeed({uri: feed.uri})
-            // await item.unsave()
-            Toast.show(_(msg`Removed from my feeds`))
-          } catch (e) {
-            Toast.show(_(msg`There was an issue contacting your server`))
-            logger.error('Failed to unsave feed', {message: e})
-          }
-        },
-      })
+      removePromptControl.open()
     } else {
-      try {
-        if (pinOnSave) {
-          await pinFeed({uri: feed.uri})
-        } else {
-          await saveFeed({uri: feed.uri})
-        }
-        Toast.show(_(msg`Added to my feeds`))
-      } catch (e) {
-        Toast.show(_(msg`There was an issue contacting your server`))
-        logger.error('Failed to save feed', {message: e})
-      }
+      await onSave()
     }
-  }, [isSaved, openModal, feed, removeFeed, saveFeed, _, pinOnSave, pinFeed])
+  }, [feed?.type, isSaved, removePromptControl, onSave])
 
   /*
    * LOAD STATE
@@ -166,25 +172,7 @@ export function FeedSourceCardLoaded({
             accessibilityRole="button"
             accessibilityLabel={_(msg`Remove from my feeds`)}
             accessibilityHint=""
-            onPress={() => {
-              openModal({
-                name: 'confirm',
-                title: _(msg`Remove from my feeds`),
-                message: _(msg`Remove this feed from my feeds?`),
-                onPressConfirm: async () => {
-                  try {
-                    await removeFeed({uri: feedUri})
-                    // await item.unsave()
-                    Toast.show(_(msg`Removed from my feeds`))
-                  } catch (e) {
-                    Toast.show(
-                      _(msg`There was an issue contacting your server`),
-                    )
-                    logger.error('Failed to unsave feed', {message: e})
-                  }
-                },
-              })
-            }}
+            onPress={onToggleSaved}
             hitSlop={15}
             style={styles.btn}>
             <FontAwesomeIcon
@@ -198,89 +186,104 @@ export function FeedSourceCardLoaded({
     )
 
   return (
-    <Pressable
-      testID={`feed-${feed.displayName}`}
-      accessibilityRole="button"
-      style={[styles.container, pal.border, style]}
-      onPress={() => {
-        if (feed.type === 'feed') {
-          navigation.push('ProfileFeed', {
-            name: feed.creatorDid,
-            rkey: new AtUri(feed.uri).rkey,
-          })
-        } else if (feed.type === 'list') {
-          navigation.push('ProfileList', {
-            name: feed.creatorDid,
-            rkey: new AtUri(feed.uri).rkey,
-          })
-        }
-      }}
-      key={feed.uri}>
-      <View style={[styles.headerContainer]}>
-        <View style={[s.mr10]}>
-          <UserAvatar type="algo" size={36} avatar={feed.avatar} />
-        </View>
-        <View style={[styles.headerTextContainer]}>
-          <Text style={[pal.text, s.bold]} numberOfLines={3}>
-            {feed.displayName}
-          </Text>
-          <Text style={[pal.textLight]} numberOfLines={3}>
-            {feed.type === 'feed' ? (
-              <Trans>Feed by {sanitizeHandle(feed.creatorHandle, '@')}</Trans>
-            ) : (
-              <Trans>List by {sanitizeHandle(feed.creatorHandle, '@')}</Trans>
-            )}
-          </Text>
-        </View>
-
-        {showSaveBtn && feed.type === 'feed' && (
-          <View style={[s.justifyCenter]}>
-            <Pressable
-              testID={`feed-${feed.displayName}-toggleSave`}
-              disabled={isSavePending || isPinPending || isRemovePending}
-              accessibilityRole="button"
-              accessibilityLabel={
-                isSaved ? _(msg`Remove from my feeds`) : _(msg`Add to my feeds`)
-              }
-              accessibilityHint=""
-              onPress={onToggleSaved}
-              hitSlop={15}
-              style={styles.btn}>
-              {isSaved ? (
-                <FontAwesomeIcon
-                  icon={['far', 'trash-can']}
-                  size={19}
-                  color={pal.colors.icon}
-                />
+    <>
+      <Pressable
+        testID={`feed-${feed.displayName}`}
+        accessibilityRole="button"
+        style={[styles.container, pal.border, style]}
+        onPress={() => {
+          if (feed.type === 'feed') {
+            navigation.push('ProfileFeed', {
+              name: feed.creatorDid,
+              rkey: new AtUri(feed.uri).rkey,
+            })
+          } else if (feed.type === 'list') {
+            navigation.push('ProfileList', {
+              name: feed.creatorDid,
+              rkey: new AtUri(feed.uri).rkey,
+            })
+          }
+        }}
+        key={feed.uri}>
+        <View style={[styles.headerContainer]}>
+          <View style={[s.mr10]}>
+            <UserAvatar type="algo" size={36} avatar={feed.avatar} />
+          </View>
+          <View style={[styles.headerTextContainer]}>
+            <Text style={[pal.text, s.bold]} numberOfLines={3}>
+              {feed.displayName}
+            </Text>
+            <Text style={[pal.textLight]} numberOfLines={3}>
+              {feed.type === 'feed' ? (
+                <Trans>Feed by {sanitizeHandle(feed.creatorHandle, '@')}</Trans>
               ) : (
-                <FontAwesomeIcon
-                  icon="plus"
-                  size={18}
-                  color={pal.colors.link}
-                />
+                <Trans>List by {sanitizeHandle(feed.creatorHandle, '@')}</Trans>
               )}
-            </Pressable>
+            </Text>
           </View>
-        )}
-      </View>
 
-      {showDescription && feed.description ? (
-        <RichText
-          style={[pal.textLight, styles.description]}
-          richText={feed.description}
-          numberOfLines={3}
-        />
-      ) : null}
+          {showSaveBtn && feed.type === 'feed' && (
+            <View style={[s.justifyCenter]}>
+              <Pressable
+                testID={`feed-${feed.displayName}-toggleSave`}
+                disabled={isSavePending || isPinPending || isRemovePending}
+                accessibilityRole="button"
+                accessibilityLabel={
+                  isSaved
+                    ? _(msg`Remove from my feeds`)
+                    : _(msg`Add to my feeds`)
+                }
+                accessibilityHint=""
+                onPress={onToggleSaved}
+                hitSlop={15}
+                style={styles.btn}>
+                {isSaved ? (
+                  <FontAwesomeIcon
+                    icon={['far', 'trash-can']}
+                    size={19}
+                    color={pal.colors.icon}
+                  />
+                ) : (
+                  <FontAwesomeIcon
+                    icon="plus"
+                    size={18}
+                    color={pal.colors.link}
+                  />
+                )}
+              </Pressable>
+            </View>
+          )}
+        </View>
+
+        {showDescription && feed.description ? (
+          <RichText
+            style={[t.atoms.text_contrast_high, styles.description]}
+            value={feed.description}
+            numberOfLines={3}
+          />
+        ) : null}
 
-      {showLikes && feed.type === 'feed' ? (
-        <Text type="sm-medium" style={[pal.text, pal.textLight]}>
-          <Trans>
-            Liked by {feed.likeCount || 0}{' '}
-            {pluralize(feed.likeCount || 0, 'user')}
-          </Trans>
-        </Text>
-      ) : null}
-    </Pressable>
+        {showLikes && feed.type === 'feed' ? (
+          <Text type="sm-medium" style={[pal.text, pal.textLight]}>
+            <Trans>
+              Liked by {feed.likeCount || 0}{' '}
+              {pluralize(feed.likeCount || 0, 'user')}
+            </Trans>
+          </Text>
+        ) : null}
+      </Pressable>
+
+      <Prompt.Basic
+        control={removePromptControl}
+        title={_(msg`Remove from my feeds?`)}
+        description={_(
+          msg`Are you sure you want to remove ${feed.displayName} from your feeds?`,
+        )}
+        onConfirm={onUnsave}
+        confirmButtonCta={_(msg`Remove`)}
+        confirmButtonColor="negative"
+      />
+    </>
   )
 }
 
diff --git a/src/view/com/home/HomeHeader.tsx b/src/view/com/home/HomeHeader.tsx
new file mode 100644
index 000000000..aa3ecb7fc
--- /dev/null
+++ b/src/view/com/home/HomeHeader.tsx
@@ -0,0 +1,67 @@
+import React from 'react'
+import {RenderTabBarFnProps} from 'view/com/pager/Pager'
+import {HomeHeaderLayout} from './HomeHeaderLayout'
+import {FeedSourceInfo} from '#/state/queries/feed'
+import {useNavigation} from '@react-navigation/native'
+import {NavigationProp} from 'lib/routes/types'
+import {isWeb} from 'platform/detection'
+import {TabBar} from '../pager/TabBar'
+import {usePalette} from '#/lib/hooks/usePalette'
+
+export function HomeHeader(
+  props: RenderTabBarFnProps & {
+    testID?: string
+    onPressSelected: () => void
+    feeds: FeedSourceInfo[]
+  },
+) {
+  const {feeds} = props
+  const navigation = useNavigation<NavigationProp>()
+  const pal = usePalette('default')
+
+  const hasPinnedCustom = React.useMemo<boolean>(() => {
+    return feeds.some(tab => tab.uri !== '')
+  }, [feeds])
+
+  const items = React.useMemo(() => {
+    const pinnedNames = feeds.map(f => f.displayName)
+    if (!hasPinnedCustom) {
+      return pinnedNames.concat('Feeds ✨')
+    }
+    return pinnedNames
+  }, [hasPinnedCustom, feeds])
+
+  const onPressFeedsLink = React.useCallback(() => {
+    if (isWeb) {
+      navigation.navigate('Feeds')
+    } else {
+      navigation.navigate('FeedsTab')
+      navigation.popToTop()
+    }
+  }, [navigation])
+
+  const onSelect = React.useCallback(
+    (index: number) => {
+      if (!hasPinnedCustom && index === items.length - 1) {
+        onPressFeedsLink()
+      } else if (props.onSelect) {
+        props.onSelect(index)
+      }
+    },
+    [items.length, onPressFeedsLink, props, hasPinnedCustom],
+  )
+
+  return (
+    <HomeHeaderLayout tabBarAnchor={props.tabBarAnchor}>
+      <TabBar
+        key={items.join(',')}
+        onPressSelected={props.onPressSelected}
+        selectedPage={props.selectedPage}
+        onSelect={onSelect}
+        testID={props.testID}
+        items={items}
+        indicatorColor={pal.colors.link}
+      />
+    </HomeHeaderLayout>
+  )
+}
diff --git a/src/view/com/home/HomeHeaderLayout.tsx b/src/view/com/home/HomeHeaderLayout.tsx
new file mode 100644
index 000000000..70bf064d4
--- /dev/null
+++ b/src/view/com/home/HomeHeaderLayout.tsx
@@ -0,0 +1 @@
+export {HomeHeaderLayoutMobile as HomeHeaderLayout} from './HomeHeaderLayoutMobile'
diff --git a/src/view/com/home/HomeHeaderLayout.web.tsx b/src/view/com/home/HomeHeaderLayout.web.tsx
new file mode 100644
index 000000000..9818b56f6
--- /dev/null
+++ b/src/view/com/home/HomeHeaderLayout.web.tsx
@@ -0,0 +1,111 @@
+import React from 'react'
+import {StyleSheet, View} from 'react-native'
+import Animated from 'react-native-reanimated'
+import {usePalette} from 'lib/hooks/usePalette'
+import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
+import {HomeHeaderLayoutMobile} from './HomeHeaderLayoutMobile'
+import {Logo} from '#/view/icons/Logo'
+import {Link} from '../util/Link'
+import {
+  FontAwesomeIcon,
+  FontAwesomeIconStyle,
+} from '@fortawesome/react-native-fontawesome'
+import {useLingui} from '@lingui/react'
+import {msg} from '@lingui/macro'
+import {CogIcon} from '#/lib/icons'
+import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode'
+import {useShellLayout} from '#/state/shell/shell-layout'
+
+export function HomeHeaderLayout(props: {
+  children: React.ReactNode
+  tabBarAnchor: JSX.Element | null | undefined
+}) {
+  const {isMobile} = useWebMediaQueries()
+  if (isMobile) {
+    return <HomeHeaderLayoutMobile {...props} />
+  } else {
+    return <HomeHeaderLayoutDesktopAndTablet {...props} />
+  }
+}
+
+function HomeHeaderLayoutDesktopAndTablet({
+  children,
+  tabBarAnchor,
+}: {
+  children: React.ReactNode
+  tabBarAnchor: JSX.Element | null | undefined
+}) {
+  const pal = usePalette('default')
+  const {headerMinimalShellTransform} = useMinimalShellMode()
+  const {headerHeight} = useShellLayout()
+  const {_} = useLingui()
+
+  return (
+    <>
+      <View style={[pal.view, pal.border, styles.bar, styles.topBar]}>
+        <Link
+          href="/settings/following-feed"
+          hitSlop={10}
+          accessibilityRole="button"
+          accessibilityLabel={_(msg`Following Feed Preferences`)}
+          accessibilityHint="">
+          <FontAwesomeIcon
+            icon="sliders"
+            style={pal.textLight as FontAwesomeIconStyle}
+          />
+        </Link>
+        <Logo width={28} />
+        <Link
+          href="/settings/saved-feeds"
+          hitSlop={10}
+          accessibilityRole="button"
+          accessibilityLabel={_(msg`Edit Saved Feeds`)}
+          accessibilityHint={_(msg`Opens screen to edit Saved Feeds`)}>
+          <CogIcon size={22} strokeWidth={2} style={pal.textLight} />
+        </Link>
+      </View>
+      {tabBarAnchor}
+      <Animated.View
+        onLayout={e => {
+          headerHeight.value = e.nativeEvent.layout.height
+        }}
+        style={[
+          pal.view,
+          pal.border,
+          styles.bar,
+          styles.tabBar,
+          headerMinimalShellTransform,
+        ]}>
+        {children}
+      </Animated.View>
+    </>
+  )
+}
+
+const styles = StyleSheet.create({
+  bar: {
+    // @ts-ignore Web only
+    left: 'calc(50% - 300px)',
+    width: 600,
+    borderLeftWidth: 1,
+    borderRightWidth: 1,
+  },
+  topBar: {
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+    alignItems: 'center',
+    paddingHorizontal: 18,
+    paddingTop: 16,
+    paddingBottom: 8,
+  },
+  tabBar: {
+    // @ts-ignore Web only
+    position: 'sticky',
+    top: 0,
+    flexDirection: 'column',
+    alignItems: 'center',
+    borderLeftWidth: 1,
+    borderRightWidth: 1,
+    zIndex: 1,
+  },
+})
diff --git a/src/view/com/pager/FeedsTabBarMobile.tsx b/src/view/com/home/HomeHeaderLayoutMobile.tsx
index 4eba241ae..d7b7231c6 100644
--- a/src/view/com/pager/FeedsTabBarMobile.tsx
+++ b/src/view/com/home/HomeHeaderLayoutMobile.tsx
@@ -1,7 +1,5 @@
 import React from 'react'
 import {StyleSheet, TouchableOpacity, View} from 'react-native'
-import {TabBar} from 'view/com/pager/TabBar'
-import {RenderTabBarFnProps} from 'view/com/pager/Pager'
 import {usePalette} from 'lib/hooks/usePalette'
 import {Link} from '../util/Link'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
@@ -13,11 +11,7 @@ import {useLingui} from '@lingui/react'
 import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode'
 import {useSetDrawerOpen} from '#/state/shell/drawer-open'
 import {useShellLayout} from '#/state/shell/shell-layout'
-import {useSession} from '#/state/session'
-import {usePinnedFeedsInfos} from '#/state/queries/feed'
 import {isWeb} from 'platform/detection'
-import {useNavigation} from '@react-navigation/native'
-import {NavigationProp} from 'lib/routes/types'
 import {Logo} from '#/view/icons/Logo'
 
 import {IS_DEV} from '#/env'
@@ -25,49 +19,18 @@ import {atoms} from '#/alf'
 import {Link as Link2} from '#/components/Link'
 import {ColorPalette_Stroke2_Corner0_Rounded as ColorPalette} from '#/components/icons/ColorPalette'
 
-export function FeedsTabBar(
-  props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void},
-) {
+export function HomeHeaderLayoutMobile({
+  children,
+}: {
+  children: React.ReactNode
+  tabBarAnchor: JSX.Element | null | undefined
+}) {
   const pal = usePalette('default')
-  const {hasSession} = useSession()
   const {_} = useLingui()
   const setDrawerOpen = useSetDrawerOpen()
-  const navigation = useNavigation<NavigationProp>()
-  const {feeds, hasPinnedCustom} = usePinnedFeedsInfos()
   const {headerHeight} = useShellLayout()
   const {headerMinimalShellTransform} = useMinimalShellMode()
 
-  const items = React.useMemo(() => {
-    if (!hasSession) return []
-
-    const pinnedNames = feeds.map(f => f.displayName)
-
-    if (!hasPinnedCustom) {
-      return pinnedNames.concat('Feeds ✨')
-    }
-    return pinnedNames
-  }, [hasSession, hasPinnedCustom, feeds])
-
-  const onPressFeedsLink = React.useCallback(() => {
-    if (isWeb) {
-      navigation.navigate('Feeds')
-    } else {
-      navigation.navigate('FeedsTab')
-      navigation.popToTop()
-    }
-  }, [navigation])
-
-  const onSelect = React.useCallback(
-    (index: number) => {
-      if (hasSession && !hasPinnedCustom && index === items.length - 1) {
-        onPressFeedsLink()
-      } else if (props.onSelect) {
-        props.onSelect(index)
-      }
-    },
-    [items.length, onPressFeedsLink, props, hasSession, hasPinnedCustom],
-  )
-
   const onPressAvi = React.useCallback(() => {
     setDrawerOpen(true)
   }, [setDrawerOpen])
@@ -113,35 +76,21 @@ export function FeedsTabBar(
               <ColorPalette size="md" />
             </Link2>
           )}
-
-          {hasSession && (
-            <Link
-              testID="viewHeaderHomeFeedPrefsBtn"
-              href="/settings/home-feed"
-              hitSlop={HITSLOP_10}
-              accessibilityRole="button"
-              accessibilityLabel={_(msg`Home Feed Preferences`)}
-              accessibilityHint="">
-              <FontAwesomeIcon
-                icon="sliders"
-                style={pal.textLight as FontAwesomeIconStyle}
-              />
-            </Link>
-          )}
+          <Link
+            testID="viewHeaderHomeFeedPrefsBtn"
+            href="/settings/following-feed"
+            hitSlop={HITSLOP_10}
+            accessibilityRole="button"
+            accessibilityLabel={_(msg`Following Feed Preferences`)}
+            accessibilityHint="">
+            <FontAwesomeIcon
+              icon="sliders"
+              style={pal.textLight as FontAwesomeIconStyle}
+            />
+          </Link>
         </View>
       </View>
-
-      {items.length > 0 && (
-        <TabBar
-          key={items.join(',')}
-          onPressSelected={props.onPressSelected}
-          selectedPage={props.selectedPage}
-          onSelect={onSelect}
-          testID={props.testID}
-          items={items}
-          indicatorColor={pal.colors.link}
-        />
-      )}
+      {children}
     </Animated.View>
   )
 }
@@ -155,7 +104,6 @@ const styles = StyleSheet.create({
     right: 0,
     top: 0,
     flexDirection: 'column',
-    borderBottomWidth: 1,
   },
   topBar: {
     flexDirection: 'row',
diff --git a/src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx b/src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx
index 3401adaff..88476c8e1 100644
--- a/src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx
+++ b/src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx
@@ -6,9 +6,17 @@
  *
  */
 import React from 'react'
-import {createHitslop} from 'lib/constants'
-import {SafeAreaView, Text, TouchableOpacity, StyleSheet} from 'react-native'
-import {t} from '@lingui/macro'
+import {
+  SafeAreaView,
+  TouchableOpacity,
+  StyleSheet,
+  ViewStyle,
+} from 'react-native'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {createHitslop} from '#/lib/constants'
 
 type Props = {
   onRequestClose: () => void
@@ -16,20 +24,23 @@ type Props = {
 
 const HIT_SLOP = createHitslop(16)
 
-const ImageDefaultHeader = ({onRequestClose}: Props) => (
-  <SafeAreaView style={styles.root}>
-    <TouchableOpacity
-      style={styles.closeButton}
-      onPress={onRequestClose}
-      hitSlop={HIT_SLOP}
-      accessibilityRole="button"
-      accessibilityLabel={t`Close image`}
-      accessibilityHint={t`Closes viewer for header image`}
-      onAccessibilityEscape={onRequestClose}>
-      <Text style={styles.closeText}>✕</Text>
-    </TouchableOpacity>
-  </SafeAreaView>
-)
+const ImageDefaultHeader = ({onRequestClose}: Props) => {
+  const {_} = useLingui()
+  return (
+    <SafeAreaView style={styles.root}>
+      <TouchableOpacity
+        style={[styles.closeButton, styles.blurredBackground]}
+        onPress={onRequestClose}
+        hitSlop={HIT_SLOP}
+        accessibilityRole="button"
+        accessibilityLabel={_(msg`Close image`)}
+        accessibilityHint={_(msg`Closes viewer for header image`)}
+        onAccessibilityEscape={onRequestClose}>
+        <FontAwesomeIcon icon="close" color={'#fff'} size={22} />
+      </TouchableOpacity>
+    </SafeAreaView>
+  )
+}
 
 const styles = StyleSheet.create({
   root: {
@@ -37,8 +48,8 @@ const styles = StyleSheet.create({
     pointerEvents: 'box-none',
   },
   closeButton: {
-    marginRight: 8,
-    marginTop: 8,
+    marginRight: 10,
+    marginTop: 10,
     width: 44,
     height: 44,
     alignItems: 'center',
@@ -46,13 +57,10 @@ const styles = StyleSheet.create({
     borderRadius: 22,
     backgroundColor: '#00000077',
   },
-  closeText: {
-    lineHeight: 22,
-    fontSize: 19,
-    textAlign: 'center',
-    color: '#FFF',
-    includeFontPadding: false,
-  },
+  blurredBackground: {
+    backdropFilter: 'blur(10px)',
+    WebkitBackdropFilter: 'blur(10px)',
+  } as ViewStyle,
 })
 
 export default ImageDefaultHeader
diff --git a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx
index 003ad61ba..414f98a61 100644
--- a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx
+++ b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx
@@ -37,6 +37,7 @@ type Props = {
   onTap: () => void
   onZoom: (isZoomed: boolean) => void
   isScrollViewBeingDragged: boolean
+  showControls: boolean
 }
 const ImageItem = ({
   imageSrc,
diff --git a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx
index cf4ba71df..383490f4f 100644
--- a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx
+++ b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx
@@ -37,11 +37,18 @@ type Props = {
   onTap: () => void
   onZoom: (scaled: boolean) => void
   isScrollViewBeingDragged: boolean
+  showControls: boolean
 }
 
 const AnimatedImage = Animated.createAnimatedComponent(Image)
 
-const ImageItem = ({imageSrc, onTap, onZoom, onRequestClose}: Props) => {
+const ImageItem = ({
+  imageSrc,
+  onTap,
+  onZoom,
+  onRequestClose,
+  showControls,
+}: Props) => {
   const scrollViewRef = useAnimatedRef<Animated.ScrollView>()
   const translationY = useSharedValue(0)
   const [loaded, setLoaded] = useState(false)
@@ -144,7 +151,7 @@ const ImageItem = ({imageSrc, onTap, onZoom, onRequestClose}: Props) => {
           accessibilityLabel={imageSrc.alt}
           accessibilityHint=""
           onLoad={() => setLoaded(true)}
-          enableLiveTextInteraction={!scaled}
+          enableLiveTextInteraction={showControls && !scaled}
         />
       </Animated.ScrollView>
     </GestureDetector>
diff --git a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.tsx b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.tsx
index 16688b820..08b99bf9e 100644
--- a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.tsx
+++ b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.tsx
@@ -10,6 +10,7 @@ type Props = {
   onTap: () => void
   onZoom: (scaled: boolean) => void
   isScrollViewBeingDragged: boolean
+  showControls: boolean
 }
 
 const ImageItem = (_props: Props) => {
diff --git a/src/view/com/lightbox/ImageViewing/index.tsx b/src/view/com/lightbox/ImageViewing/index.tsx
index b6835793d..ff8fdb86d 100644
--- a/src/view/com/lightbox/ImageViewing/index.tsx
+++ b/src/view/com/lightbox/ImageViewing/index.tsx
@@ -122,6 +122,7 @@ function ImageViewing({
                 imageSrc={imageSrc}
                 onRequestClose={onRequestClose}
                 isScrollViewBeingDragged={isDragging}
+                showControls={showControls}
               />
             </View>
           ))}
diff --git a/src/view/com/lightbox/Lightbox.tsx b/src/view/com/lightbox/Lightbox.tsx
index 2ee5b8d59..5bab643ca 100644
--- a/src/view/com/lightbox/Lightbox.tsx
+++ b/src/view/com/lightbox/Lightbox.tsx
@@ -78,9 +78,9 @@ function LightboxFooter({imageIndex}: {imageIndex: number}) {
 
       try {
         await saveImageToMediaLibrary({uri})
-        Toast.show('Saved to your camera roll.')
+        Toast.show(_(msg`Saved to your camera roll.`))
       } catch (e: any) {
-        Toast.show(`Failed to save image: ${String(e)}`)
+        Toast.show(_(msg`Failed to save image: ${String(e)}`))
       }
     },
     [permissionResponse, requestPermission, _],
diff --git a/src/view/com/lightbox/Lightbox.web.tsx b/src/view/com/lightbox/Lightbox.web.tsx
index fb97c30a4..942c9a686 100644
--- a/src/view/com/lightbox/Lightbox.web.tsx
+++ b/src/view/com/lightbox/Lightbox.web.tsx
@@ -7,6 +7,7 @@ import {
   StyleSheet,
   View,
   Pressable,
+  ViewStyle,
 } from 'react-native'
 import {
   FontAwesomeIcon,
@@ -24,6 +25,7 @@ import {
   ProfileImageLightbox,
 } from '#/state/lightbox'
 import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock'
+import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
 
 interface Img {
   uri: string
@@ -111,6 +113,14 @@ function LightboxInner({
     return () => window.removeEventListener('keydown', onKeyDown)
   }, [onKeyDown])
 
+  const {isTabletOrDesktop} = useWebMediaQueries()
+  const btnStyle = React.useMemo(() => {
+    return isTabletOrDesktop ? styles.btnTablet : styles.btnMobile
+  }, [isTabletOrDesktop])
+  const iconSize = React.useMemo(() => {
+    return isTabletOrDesktop ? 32 : 24
+  }, [isTabletOrDesktop])
+
   return (
     <View style={styles.mask}>
       <TouchableWithoutFeedback
@@ -130,28 +140,38 @@ function LightboxInner({
           {canGoLeft && (
             <TouchableOpacity
               onPress={onPressLeft}
-              style={[styles.btn, styles.leftBtn]}
+              style={[
+                styles.btn,
+                btnStyle,
+                styles.leftBtn,
+                styles.blurredBackground,
+              ]}
               accessibilityRole="button"
               accessibilityLabel={_(msg`Previous image`)}
               accessibilityHint="">
               <FontAwesomeIcon
                 icon="angle-left"
                 style={styles.icon as FontAwesomeIconStyle}
-                size={40}
+                size={iconSize}
               />
             </TouchableOpacity>
           )}
           {canGoRight && (
             <TouchableOpacity
               onPress={onPressRight}
-              style={[styles.btn, styles.rightBtn]}
+              style={[
+                styles.btn,
+                btnStyle,
+                styles.rightBtn,
+                styles.blurredBackground,
+              ]}
               accessibilityRole="button"
               accessibilityLabel={_(msg`Next image`)}
               accessibilityHint="">
               <FontAwesomeIcon
                 icon="angle-right"
                 style={styles.icon as FontAwesomeIconStyle}
-                size={40}
+                size={iconSize}
               />
             </TouchableOpacity>
           )}
@@ -213,20 +233,30 @@ const styles = StyleSheet.create({
   },
   btn: {
     position: 'absolute',
-    backgroundColor: '#000',
-    width: 50,
-    height: 50,
+    backgroundColor: '#00000077',
     justifyContent: 'center',
     alignItems: 'center',
+  },
+  btnTablet: {
+    width: 50,
+    height: 50,
     borderRadius: 25,
+    left: 30,
+    right: 30,
+  },
+  btnMobile: {
+    width: 44,
+    height: 44,
+    borderRadius: 22,
+    left: 20,
+    right: 20,
   },
   leftBtn: {
-    left: 30,
+    right: 'auto',
     top: '50%',
   },
   rightBtn: {
-    position: 'absolute',
-    right: 30,
+    left: 'auto',
     top: '50%',
   },
   footer: {
@@ -234,4 +264,8 @@ const styles = StyleSheet.create({
     paddingVertical: 24,
     backgroundColor: colors.black,
   },
+  blurredBackground: {
+    backdropFilter: 'blur(10px)',
+    WebkitBackdropFilter: 'blur(10px)',
+  } as ViewStyle,
 })
diff --git a/src/view/com/lists/ListCard.tsx b/src/view/com/lists/ListCard.tsx
index 5750faec1..19842eb54 100644
--- a/src/view/com/lists/ListCard.tsx
+++ b/src/view/com/lists/ListCard.tsx
@@ -3,7 +3,7 @@ import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
 import {AtUri, AppBskyGraphDefs, RichText} from '@atproto/api'
 import {Link} from '../util/Link'
 import {Text} from '../util/text/Text'
-import {RichText as RichTextCom} from '../util/text/RichText'
+import {RichText as RichTextCom} from '#/components/RichText'
 import {UserAvatar} from '../util/UserAvatar'
 import {s} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
@@ -12,6 +12,7 @@ import {sanitizeDisplayName} from 'lib/strings/display-names'
 import {sanitizeHandle} from 'lib/strings/handles'
 import {makeProfileLink} from 'lib/routes/links'
 import {Trans} from '@lingui/macro'
+import {atoms as a} from '#/alf'
 
 export const ListCard = ({
   testID,
@@ -119,9 +120,9 @@ export const ListCard = ({
       {descriptionRichText ? (
         <View style={styles.details}>
           <RichTextCom
-            style={[pal.text, s.flex1]}
+            style={[a.flex_1]}
             numberOfLines={20}
-            richText={descriptionRichText}
+            value={descriptionRichText}
           />
         </View>
       ) : undefined}
diff --git a/src/view/com/modals/AppealLabel.tsx b/src/view/com/modals/AppealLabel.tsx
deleted file mode 100644
index b0aaaf625..000000000
--- a/src/view/com/modals/AppealLabel.tsx
+++ /dev/null
@@ -1,139 +0,0 @@
-import React, {useState} from 'react'
-import {StyleSheet, TouchableOpacity, View} from 'react-native'
-import {ComAtprotoModerationDefs} from '@atproto/api'
-import {ScrollView, TextInput} from './util'
-import {Text} from '../util/text/Text'
-import {s, colors} from 'lib/styles'
-import {usePalette} from 'lib/hooks/usePalette'
-import {Trans, msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {useModalControls} from '#/state/modals'
-import {CharProgress} from '../composer/char-progress/CharProgress'
-import {getAgent} from '#/state/session'
-import * as Toast from '../util/Toast'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
-
-export const snapPoints = ['40%']
-
-type ReportComponentProps =
-  | {
-      uri: string
-      cid: string
-    }
-  | {
-      did: string
-    }
-
-export function Component(props: ReportComponentProps) {
-  const pal = usePalette('default')
-  const [details, setDetails] = useState<string>('')
-  const {_} = useLingui()
-  const {closeModal} = useModalControls()
-  const {isMobile} = useWebMediaQueries()
-  const isAccountReport = 'did' in props
-
-  const submit = async () => {
-    try {
-      const $type = !isAccountReport
-        ? 'com.atproto.repo.strongRef'
-        : 'com.atproto.admin.defs#repoRef'
-      await getAgent().createModerationReport({
-        reasonType: ComAtprotoModerationDefs.REASONAPPEAL,
-        subject: {
-          $type,
-          ...props,
-        },
-        reason: details,
-      })
-      Toast.show(_(msg`We'll look into your appeal promptly.`))
-    } finally {
-      closeModal()
-    }
-  }
-
-  return (
-    <View
-      style={[
-        pal.view,
-        s.flex1,
-        isMobile ? {paddingHorizontal: 12} : undefined,
-      ]}
-      testID="appealLabelModal">
-      <Text
-        type="2xl-bold"
-        style={[pal.text, s.textCenter, {paddingBottom: 8}]}>
-        <Trans>Appeal Content Warning</Trans>
-      </Text>
-      <ScrollView>
-        <View style={[pal.btn, styles.detailsInputContainer]}>
-          <TextInput
-            accessibilityLabel={_(msg`Text input field`)}
-            accessibilityHint={_(
-              msg`Please tell us why you think this content warning was incorrectly applied!`,
-            )}
-            placeholder={_(
-              msg`Please tell us why you think this content warning was incorrectly applied!`,
-            )}
-            placeholderTextColor={pal.textLight.color}
-            value={details}
-            onChangeText={setDetails}
-            autoFocus={true}
-            numberOfLines={3}
-            multiline={true}
-            textAlignVertical="top"
-            maxLength={300}
-            style={[styles.detailsInput, pal.text]}
-          />
-          <View style={styles.detailsInputBottomBar}>
-            <View style={styles.charCounter}>
-              <CharProgress count={details?.length || 0} />
-            </View>
-          </View>
-        </View>
-        <TouchableOpacity
-          testID="confirmBtn"
-          onPress={submit}
-          style={styles.btn}
-          accessibilityRole="button"
-          accessibilityLabel={_(msg`Confirm`)}
-          accessibilityHint="">
-          <Text style={[s.white, s.bold, s.f18]}>
-            <Trans>Submit</Trans>
-          </Text>
-        </TouchableOpacity>
-      </ScrollView>
-    </View>
-  )
-}
-
-const styles = StyleSheet.create({
-  detailsInputContainer: {
-    borderRadius: 8,
-    marginBottom: 8,
-  },
-  detailsInput: {
-    paddingHorizontal: 12,
-    paddingTop: 12,
-    paddingBottom: 12,
-    borderRadius: 8,
-    minHeight: 100,
-    fontSize: 16,
-  },
-  detailsInputBottomBar: {
-    alignSelf: 'flex-end',
-  },
-  charCounter: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    paddingRight: 10,
-    paddingBottom: 8,
-  },
-  btn: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    justifyContent: 'center',
-    borderRadius: 32,
-    padding: 14,
-    backgroundColor: colors.blue3,
-  },
-})
diff --git a/src/view/com/modals/BirthDateSettings.tsx b/src/view/com/modals/BirthDateSettings.tsx
deleted file mode 100644
index 1cab95989..000000000
--- a/src/view/com/modals/BirthDateSettings.tsx
+++ /dev/null
@@ -1,151 +0,0 @@
-import React, {useState} from 'react'
-import {
-  ActivityIndicator,
-  StyleSheet,
-  TouchableOpacity,
-  View,
-} from 'react-native'
-import {Text} from '../util/text/Text'
-import {DateInput} from '../util/forms/DateInput'
-import {ErrorMessage} from '../util/error/ErrorMessage'
-import {s, colors} from 'lib/styles'
-import {usePalette} from 'lib/hooks/usePalette'
-import {isWeb} from 'platform/detection'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {cleanError} from 'lib/strings/errors'
-import {Trans, msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {useModalControls} from '#/state/modals'
-import {
-  usePreferencesQuery,
-  usePreferencesSetBirthDateMutation,
-  UsePreferencesQueryResponse,
-} from '#/state/queries/preferences'
-import {logger} from '#/logger'
-
-export const snapPoints = ['50%', '90%']
-
-function Inner({preferences}: {preferences: UsePreferencesQueryResponse}) {
-  const pal = usePalette('default')
-  const {isMobile} = useWebMediaQueries()
-  const {_} = useLingui()
-  const {
-    isPending,
-    isError,
-    error,
-    mutateAsync: setBirthDate,
-  } = usePreferencesSetBirthDateMutation()
-  const [date, setDate] = useState(preferences.birthDate || new Date())
-  const {closeModal} = useModalControls()
-
-  const onSave = React.useCallback(async () => {
-    try {
-      await setBirthDate({birthDate: date})
-      closeModal()
-    } catch (e) {
-      logger.error(`setBirthDate failed`, {message: e})
-    }
-  }, [date, setBirthDate, closeModal])
-
-  return (
-    <View
-      testID="birthDateSettingsModal"
-      style={[pal.view, styles.container, isMobile && {paddingHorizontal: 18}]}>
-      <View style={styles.titleSection}>
-        <Text type="title-lg" style={[pal.text, styles.title]}>
-          <Trans>My Birthday</Trans>
-        </Text>
-      </View>
-
-      <Text type="lg" style={[pal.textLight, {marginBottom: 10}]}>
-        <Trans>This information is not shared with other users.</Trans>
-      </Text>
-
-      <View>
-        <DateInput
-          handleAsUTC
-          testID="birthdayInput"
-          value={date}
-          onChange={setDate}
-          buttonType="default-light"
-          buttonStyle={[pal.border, styles.dateInputButton]}
-          buttonLabelType="lg"
-          accessibilityLabel={_(msg`Birthday`)}
-          accessibilityHint={_(msg`Enter your birth date`)}
-          accessibilityLabelledBy="birthDate"
-        />
-      </View>
-
-      {isError ? (
-        <ErrorMessage message={cleanError(error)} style={styles.error} />
-      ) : undefined}
-
-      <View style={[styles.btnContainer, pal.borderDark]}>
-        {isPending ? (
-          <View style={styles.btn}>
-            <ActivityIndicator color="#fff" />
-          </View>
-        ) : (
-          <TouchableOpacity
-            testID="confirmBtn"
-            onPress={onSave}
-            style={styles.btn}
-            accessibilityRole="button"
-            accessibilityLabel={_(msg`Save`)}
-            accessibilityHint="">
-            <Text style={[s.white, s.bold, s.f18]}>
-              <Trans>Save</Trans>
-            </Text>
-          </TouchableOpacity>
-        )}
-      </View>
-    </View>
-  )
-}
-
-export function Component({}: {}) {
-  const {data: preferences} = usePreferencesQuery()
-
-  return !preferences ? (
-    <ActivityIndicator />
-  ) : (
-    <Inner preferences={preferences} />
-  )
-}
-
-const styles = StyleSheet.create({
-  container: {
-    flex: 1,
-    paddingBottom: isWeb ? 0 : 40,
-  },
-  titleSection: {
-    paddingTop: isWeb ? 0 : 4,
-    paddingBottom: isWeb ? 14 : 10,
-  },
-  title: {
-    textAlign: 'center',
-    fontWeight: '600',
-    marginBottom: 5,
-  },
-  error: {
-    borderRadius: 6,
-    marginTop: 10,
-  },
-  dateInputButton: {
-    borderWidth: 1,
-    borderRadius: 6,
-    paddingVertical: 14,
-  },
-  btn: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    justifyContent: 'center',
-    borderRadius: 32,
-    padding: 14,
-    backgroundColor: colors.blue3,
-  },
-  btnContainer: {
-    paddingTop: 20,
-    paddingHorizontal: 20,
-  },
-})
diff --git a/src/view/com/modals/ChangeHandle.tsx b/src/view/com/modals/ChangeHandle.tsx
index a43c30c29..f04bdb0e4 100644
--- a/src/view/com/modals/ChangeHandle.tsx
+++ b/src/view/com/modals/ChangeHandle.tsx
@@ -150,7 +150,7 @@ export function Inner({
             accessibilityHint={_(msg`Exits handle change process`)}
             onAccessibilityEscape={onPressCancel}>
             <Text type="lg" style={pal.textLight}>
-              Cancel
+              <Trans>Cancel</Trans>
             </Text>
           </TouchableOpacity>
         </View>
@@ -254,7 +254,7 @@ function ProvidedHandleForm({
         <TextInput
           testID="setHandleInput"
           style={[pal.text, styles.textInput]}
-          placeholder="e.g. alice"
+          placeholder={_(msg`e.g. alice`)}
           placeholderTextColor={pal.colors.textLight}
           autoCapitalize="none"
           keyboardAppearance={theme.colorScheme}
@@ -277,8 +277,8 @@ function ProvidedHandleForm({
       <TouchableOpacity
         onPress={onToggleCustom}
         accessibilityRole="button"
-        accessibilityHint="Hosting provider"
-        accessibilityLabel={_(msg`Opens modal for using custom domain`)}>
+        accessibilityLabel={_(msg`Hosting provider`)}
+        accessibilityHint={_(msg`Opens modal for using custom domain`)}>
         <Text type="md-medium" style={[pal.link, s.pl10, s.pt5]}>
           <Trans>I have my own domain</Trans>
         </Text>
@@ -324,8 +324,8 @@ function CustomHandleForm({
     Clipboard.setString(
       isDNSForm ? `did=${currentAccount.did}` : currentAccount.did,
     )
-    Toast.show('Copied to clipboard')
-  }, [currentAccount, isDNSForm])
+    Toast.show(_(msg`Copied to clipboard`))
+  }, [currentAccount, isDNSForm, _])
   const onChangeHandle = React.useCallback(
     (v: string) => {
       setHandle(v)
@@ -378,7 +378,7 @@ function CustomHandleForm({
         <TextInput
           testID="setHandleInput"
           style={[pal.text, styles.textInput]}
-          placeholder="e.g. alice.com"
+          placeholder={_(msg`e.g. alice.com`)}
           placeholderTextColor={pal.colors.textLight}
           autoCapitalize="none"
           keyboardAppearance={theme.colorScheme}
@@ -387,7 +387,7 @@ function CustomHandleForm({
           editable={!isProcessing}
           accessibilityLabelledBy="customDomain"
           accessibilityLabel={_(msg`Custom domain`)}
-          accessibilityHint="Input your preferred hosting provider"
+          accessibilityHint={_(msg`Input your preferred hosting provider`)}
         />
       </View>
       <View style={styles.spacer} />
@@ -395,18 +395,18 @@ function CustomHandleForm({
       <View style={[styles.selectableBtns]}>
         <SelectableBtn
           selected={isDNSForm}
-          label="DNS Panel"
+          label={_(msg`DNS Panel`)}
           left
           onSelect={() => setDNSForm(true)}
-          accessibilityHint="Use the DNS panel"
+          accessibilityHint={_(msg`Use the DNS panel`)}
           style={s.flex1}
         />
         <SelectableBtn
           selected={!isDNSForm}
-          label="No DNS Panel"
+          label={_(msg`No DNS Panel`)}
           right
           onSelect={() => setDNSForm(false)}
-          accessibilityHint="Use a file on your server"
+          accessibilityHint={_(msg`Use a file on your server`)}
           style={s.flex1}
         />
       </View>
@@ -418,7 +418,7 @@ function CustomHandleForm({
           </Text>
           <View style={[styles.dnsTable, pal.btn]}>
             <Text type="md-medium" style={[styles.dnsLabel, pal.text]}>
-              Host:
+              <Trans>Host:</Trans>
             </Text>
             <View style={[styles.dnsValue]}>
               <Text type="mono" style={[styles.monoText, pal.text]}>
@@ -426,7 +426,7 @@ function CustomHandleForm({
               </Text>
             </View>
             <Text type="md-medium" style={[styles.dnsLabel, pal.text]}>
-              Type:
+              <Trans>Type:</Trans>
             </Text>
             <View style={[styles.dnsValue]}>
               <Text type="mono" style={[styles.monoText, pal.text]}>
@@ -434,7 +434,7 @@ function CustomHandleForm({
               </Text>
             </View>
             <Text type="md-medium" style={[styles.dnsLabel, pal.text]}>
-              Value:
+              <Trans>Value:</Trans>
             </Text>
             <View style={[styles.dnsValue]}>
               <Text type="mono" style={[styles.monoText, pal.text]}>
@@ -443,7 +443,7 @@ function CustomHandleForm({
             </View>
           </View>
           <Text type="md" style={[pal.text, s.pt20, s.pl5]}>
-            This should create a domain record at:{' '}
+            <Trans>This should create a domain record at:</Trans>
           </Text>
           <Text type="mono" style={[styles.monoText, pal.text, s.pt5, s.pl5]}>
             _atproto.{handle}
@@ -463,7 +463,7 @@ function CustomHandleForm({
           </View>
           <View style={styles.spacer} />
           <Text type="md" style={[pal.text, s.pb5, s.pl5]}>
-            That contains the following:
+            <Trans>That contains the following:</Trans>
           </Text>
           <View style={[styles.valueContainer, pal.btn]}>
             <View style={[styles.dnsValue]}>
@@ -478,7 +478,9 @@ function CustomHandleForm({
       <View style={styles.spacer} />
       <Button type="default" style={[s.p20, s.mb10]} onPress={onPressCopy}>
         <Text type="xl" style={[pal.link, s.textCenter]}>
-          Copy {isDNSForm ? 'Domain Value' : 'File Contents'}
+          <Trans>
+            Copy {isDNSForm ? _(msg`Domain Value`) : _(msg`File Contents`)}
+          </Trans>
         </Text>
       </Button>
       {canSave === true && (
@@ -504,8 +506,8 @@ function CustomHandleForm({
         ) : (
           <Text type="xl-medium" style={[s.white, s.textCenter]}>
             {canSave
-              ? `Update to ${handle}`
-              : `Verify ${isDNSForm ? 'DNS Record' : 'Text File'}`}
+              ? _(msg`Update to ${handle}`)
+              : _(msg`Verify ${isDNSForm ? 'DNS Record' : 'Text File'}`)}
           </Text>
         )}
       </Button>
@@ -513,9 +515,9 @@ function CustomHandleForm({
       <TouchableOpacity
         onPress={onToggleCustom}
         accessibilityLabel={_(msg`Use default provider`)}
-        accessibilityHint="Use bsky.social as hosting provider">
+        accessibilityHint={_(msg`Use bsky.social as hosting provider`)}>
         <Text type="md-medium" style={[pal.link, s.pl10, s.pt5]}>
-          Nevermind, create a handle for me
+          <Trans>Nevermind, create a handle for me</Trans>
         </Text>
       </TouchableOpacity>
     </>
diff --git a/src/view/com/modals/ChangePassword.tsx b/src/view/com/modals/ChangePassword.tsx
index d8add9794..4badc88aa 100644
--- a/src/view/com/modals/ChangePassword.tsx
+++ b/src/view/com/modals/ChangePassword.tsx
@@ -137,7 +137,9 @@ export function Component() {
         <View>
           <View style={styles.titleSection}>
             <Text type="title-lg" style={[pal.text, styles.title]}>
-              {stage !== Stages.Done ? 'Change Password' : 'Password Changed'}
+              {stage !== Stages.Done
+                ? _(msg`Change Password`)
+                : _(msg`Password Changed`)}
             </Text>
           </View>
 
@@ -180,7 +182,7 @@ export function Component() {
                 <TextInput
                   testID="codeInput"
                   style={[pal.text, styles.textInput]}
-                  placeholder="Reset code"
+                  placeholder={_(msg`Reset code`)}
                   placeholderTextColor={pal.colors.textLight}
                   value={resetCode}
                   onChangeText={setResetCode}
@@ -207,7 +209,7 @@ export function Component() {
                 <TextInput
                   testID="codeInput"
                   style={[pal.text, styles.textInput]}
-                  placeholder="New password"
+                  placeholder={_(msg`New password`)}
                   placeholderTextColor={pal.colors.textLight}
                   onChangeText={setNewPassword}
                   secureTextEntry
diff --git a/src/view/com/modals/Confirm.tsx b/src/view/com/modals/Confirm.tsx
deleted file mode 100644
index 307897fb8..000000000
--- a/src/view/com/modals/Confirm.tsx
+++ /dev/null
@@ -1,132 +0,0 @@
-import React, {useState} from 'react'
-import {
-  ActivityIndicator,
-  StyleSheet,
-  TouchableOpacity,
-  View,
-} from 'react-native'
-import {Text} from '../util/text/Text'
-import {s, colors} from 'lib/styles'
-import {ErrorMessage} from '../util/error/ErrorMessage'
-import {cleanError} from 'lib/strings/errors'
-import {usePalette} from 'lib/hooks/usePalette'
-import {isWeb} from 'platform/detection'
-import {useLingui} from '@lingui/react'
-import {Trans, msg} from '@lingui/macro'
-import type {ConfirmModal} from '#/state/modals'
-import {useModalControls} from '#/state/modals'
-
-export const snapPoints = ['50%']
-
-export function Component({
-  title,
-  message,
-  onPressConfirm,
-  onPressCancel,
-  confirmBtnText,
-  confirmBtnStyle,
-  cancelBtnText,
-}: ConfirmModal) {
-  const pal = usePalette('default')
-  const {_} = useLingui()
-  const {closeModal} = useModalControls()
-  const [isProcessing, setIsProcessing] = useState<boolean>(false)
-  const [error, setError] = useState<string>('')
-  const onPress = async () => {
-    setError('')
-    setIsProcessing(true)
-    try {
-      await onPressConfirm()
-      closeModal()
-      return
-    } catch (e: any) {
-      setError(cleanError(e))
-      setIsProcessing(false)
-    }
-  }
-  return (
-    <View testID="confirmModal" style={[pal.view, styles.container]}>
-      <Text type="title-xl" style={[pal.text, styles.title]}>
-        {title}
-      </Text>
-      {typeof message === 'string' ? (
-        <Text type="xl" style={[pal.textLight, styles.description]}>
-          {message}
-        </Text>
-      ) : (
-        message()
-      )}
-      {error ? (
-        <View style={s.mt10}>
-          <ErrorMessage message={error} />
-        </View>
-      ) : undefined}
-      <View style={s.flex1} />
-      {isProcessing ? (
-        <View style={[styles.btn, s.mt10]}>
-          <ActivityIndicator />
-        </View>
-      ) : (
-        <TouchableOpacity
-          testID="confirmBtn"
-          onPress={onPress}
-          style={[styles.btn, confirmBtnStyle]}
-          accessibilityRole="button"
-          accessibilityLabel={_(msg({message: 'Confirm', context: 'action'}))}
-          accessibilityHint="">
-          <Text style={[s.white, s.bold, s.f18]}>
-            {confirmBtnText ?? <Trans context="action">Confirm</Trans>}
-          </Text>
-        </TouchableOpacity>
-      )}
-      {onPressCancel === undefined ? null : (
-        <TouchableOpacity
-          testID="cancelBtn"
-          onPress={onPressCancel}
-          style={[styles.btnCancel, s.mt10]}
-          accessibilityRole="button"
-          accessibilityLabel={_(msg({message: 'Cancel', context: 'action'}))}
-          accessibilityHint="">
-          <Text type="button-lg" style={pal.textLight}>
-            {cancelBtnText ?? <Trans context="action">Cancel</Trans>}
-          </Text>
-        </TouchableOpacity>
-      )}
-    </View>
-  )
-}
-
-const styles = StyleSheet.create({
-  container: {
-    flex: 1,
-    padding: 10,
-    paddingBottom: isWeb ? 0 : 60,
-  },
-  title: {
-    textAlign: 'center',
-    marginBottom: 12,
-  },
-  description: {
-    textAlign: 'center',
-    paddingHorizontal: 22,
-    marginBottom: 10,
-  },
-  btn: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    justifyContent: 'center',
-    borderRadius: 32,
-    padding: 14,
-    marginTop: 22,
-    marginHorizontal: 44,
-    backgroundColor: colors.blue3,
-  },
-  btnCancel: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    justifyContent: 'center',
-    borderRadius: 32,
-    padding: 14,
-    marginHorizontal: 20,
-  },
-})
diff --git a/src/view/com/modals/ContentFilteringSettings.tsx b/src/view/com/modals/ContentFilteringSettings.tsx
deleted file mode 100644
index 328d23dc2..000000000
--- a/src/view/com/modals/ContentFilteringSettings.tsx
+++ /dev/null
@@ -1,401 +0,0 @@
-import React from 'react'
-import {LabelPreference} from '@atproto/api'
-import {StyleSheet, Pressable, View, Linking} from 'react-native'
-import LinearGradient from 'react-native-linear-gradient'
-import {ScrollView} from './util'
-import {s, colors, gradients} from 'lib/styles'
-import {Text} from '../util/text/Text'
-import {TextLink} from '../util/Link'
-import {ToggleButton} from '../util/forms/ToggleButton'
-import {Button} from '../util/forms/Button'
-import {usePalette} from 'lib/hooks/usePalette'
-import {isIOS} from 'platform/detection'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import * as Toast from '../util/Toast'
-import {logger} from '#/logger'
-import {Trans, msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {useModalControls} from '#/state/modals'
-import {
-  usePreferencesQuery,
-  usePreferencesSetContentLabelMutation,
-  usePreferencesSetAdultContentMutation,
-  ConfigurableLabelGroup,
-  CONFIGURABLE_LABEL_GROUPS,
-  UsePreferencesQueryResponse,
-} from '#/state/queries/preferences'
-
-export const snapPoints = ['90%']
-
-export function Component({}: {}) {
-  const {isMobile} = useWebMediaQueries()
-  const pal = usePalette('default')
-  const {_} = useLingui()
-  const {closeModal} = useModalControls()
-  const {data: preferences} = usePreferencesQuery()
-
-  const onPressDone = React.useCallback(() => {
-    closeModal()
-  }, [closeModal])
-
-  return (
-    <View testID="contentFilteringModal" style={[pal.view, styles.container]}>
-      <Text style={[pal.text, styles.title]}>
-        <Trans>Content Filtering</Trans>
-      </Text>
-
-      <ScrollView style={styles.scrollContainer}>
-        <AdultContentEnabledPref />
-        <ContentLabelPref
-          preferences={preferences}
-          labelGroup="nsfw"
-          disabled={!preferences?.adultContentEnabled}
-        />
-        <ContentLabelPref
-          preferences={preferences}
-          labelGroup="nudity"
-          disabled={!preferences?.adultContentEnabled}
-        />
-        <ContentLabelPref
-          preferences={preferences}
-          labelGroup="suggestive"
-          disabled={!preferences?.adultContentEnabled}
-        />
-        <ContentLabelPref
-          preferences={preferences}
-          labelGroup="gore"
-          disabled={!preferences?.adultContentEnabled}
-        />
-        <ContentLabelPref preferences={preferences} labelGroup="hate" />
-        <ContentLabelPref preferences={preferences} labelGroup="spam" />
-        <ContentLabelPref
-          preferences={preferences}
-          labelGroup="impersonation"
-        />
-        <View style={{height: isMobile ? 60 : 0}} />
-      </ScrollView>
-
-      <View
-        style={[
-          styles.btnContainer,
-          isMobile && styles.btnContainerMobile,
-          pal.borderDark,
-        ]}>
-        <Pressable
-          testID="sendReportBtn"
-          onPress={onPressDone}
-          accessibilityRole="button"
-          accessibilityLabel={_(msg`Done`)}
-          accessibilityHint="">
-          <LinearGradient
-            colors={[gradients.blueLight.start, gradients.blueLight.end]}
-            start={{x: 0, y: 0}}
-            end={{x: 1, y: 1}}
-            style={[styles.btn]}>
-            <Text style={[s.white, s.bold, s.f18]}>
-              <Trans>Done</Trans>
-            </Text>
-          </LinearGradient>
-        </Pressable>
-      </View>
-    </View>
-  )
-}
-
-function AdultContentEnabledPref() {
-  const pal = usePalette('default')
-  const {_} = useLingui()
-  const {data: preferences} = usePreferencesQuery()
-  const {mutate, variables} = usePreferencesSetAdultContentMutation()
-  const {openModal} = useModalControls()
-
-  const onSetAge = React.useCallback(
-    () => openModal({name: 'birth-date-settings'}),
-    [openModal],
-  )
-
-  const onToggleAdultContent = React.useCallback(async () => {
-    if (isIOS) return
-
-    try {
-      mutate({
-        enabled: !(variables?.enabled ?? preferences?.adultContentEnabled),
-      })
-    } catch (e) {
-      Toast.show(
-        _(msg`There was an issue syncing your preferences with the server`),
-      )
-      logger.error('Failed to update preferences with server', {message: e})
-    }
-  }, [variables, preferences, mutate, _])
-
-  const onAdultContentLinkPress = React.useCallback(() => {
-    Linking.openURL('https://bsky.app/')
-  }, [])
-
-  return (
-    <View style={s.mb10}>
-      {isIOS ? (
-        preferences?.adultContentEnabled ? null : (
-          <Text type="md" style={pal.textLight}>
-            <Trans>
-              Adult content can only be enabled via the Web at{' '}
-              <TextLink
-                style={pal.link}
-                href=""
-                text="bsky.app"
-                onPress={onAdultContentLinkPress}
-              />
-              .
-            </Trans>
-          </Text>
-        )
-      ) : typeof preferences?.birthDate === 'undefined' ? (
-        <View style={[pal.viewLight, styles.agePrompt]}>
-          <Text type="md" style={[pal.text, {flex: 1}]}>
-            <Trans>Confirm your age to enable adult content.</Trans>
-          </Text>
-          <Button
-            type="primary"
-            label={_(msg({message: 'Set Age', context: 'action'}))}
-            onPress={onSetAge}
-          />
-        </View>
-      ) : (preferences.userAge || 0) >= 18 ? (
-        <ToggleButton
-          type="default-light"
-          label={_(msg`Enable Adult Content`)}
-          isSelected={variables?.enabled ?? preferences?.adultContentEnabled}
-          onPress={onToggleAdultContent}
-          style={styles.toggleBtn}
-        />
-      ) : (
-        <View style={[pal.viewLight, styles.agePrompt]}>
-          <Text type="md" style={[pal.text, {flex: 1}]}>
-            <Trans>You must be 18 or older to enable adult content.</Trans>
-          </Text>
-          <Button
-            type="primary"
-            label={_(msg({message: 'Set Age', context: 'action'}))}
-            onPress={onSetAge}
-          />
-        </View>
-      )}
-    </View>
-  )
-}
-
-// TODO: Refactor this component to pass labels down to each tab
-function ContentLabelPref({
-  preferences,
-  labelGroup,
-  disabled,
-}: {
-  preferences?: UsePreferencesQueryResponse
-  labelGroup: ConfigurableLabelGroup
-  disabled?: boolean
-}) {
-  const pal = usePalette('default')
-  const visibility = preferences?.contentLabels?.[labelGroup]
-  const {mutate, variables} = usePreferencesSetContentLabelMutation()
-
-  const onChange = React.useCallback(
-    (vis: LabelPreference) => {
-      mutate({labelGroup, visibility: vis})
-    },
-    [mutate, labelGroup],
-  )
-
-  return (
-    <View style={[styles.contentLabelPref, pal.border]}>
-      <View style={s.flex1}>
-        <Text type="md-medium" style={[pal.text]}>
-          {CONFIGURABLE_LABEL_GROUPS[labelGroup].title}
-        </Text>
-        {typeof CONFIGURABLE_LABEL_GROUPS[labelGroup].subtitle === 'string' && (
-          <Text type="sm" style={[pal.textLight]}>
-            {CONFIGURABLE_LABEL_GROUPS[labelGroup].subtitle}
-          </Text>
-        )}
-      </View>
-
-      {disabled || !visibility ? (
-        <Text type="sm-bold" style={pal.textLight}>
-          <Trans context="action">Hide</Trans>
-        </Text>
-      ) : (
-        <SelectGroup
-          current={variables?.visibility || visibility}
-          onChange={onChange}
-          labelGroup={labelGroup}
-        />
-      )}
-    </View>
-  )
-}
-
-interface SelectGroupProps {
-  current: LabelPreference
-  onChange: (v: LabelPreference) => void
-  labelGroup: ConfigurableLabelGroup
-}
-
-function SelectGroup({current, onChange, labelGroup}: SelectGroupProps) {
-  const {_} = useLingui()
-
-  return (
-    <View style={styles.selectableBtns}>
-      <SelectableBtn
-        current={current}
-        value="hide"
-        label={_(msg`Hide`)}
-        left
-        onChange={onChange}
-        labelGroup={labelGroup}
-      />
-      <SelectableBtn
-        current={current}
-        value="warn"
-        label={_(msg`Warn`)}
-        onChange={onChange}
-        labelGroup={labelGroup}
-      />
-      <SelectableBtn
-        current={current}
-        value="ignore"
-        label={_(msg`Show`)}
-        right
-        onChange={onChange}
-        labelGroup={labelGroup}
-      />
-    </View>
-  )
-}
-
-interface SelectableBtnProps {
-  current: string
-  value: LabelPreference
-  label: string
-  left?: boolean
-  right?: boolean
-  onChange: (v: LabelPreference) => void
-  labelGroup: ConfigurableLabelGroup
-}
-
-function SelectableBtn({
-  current,
-  value,
-  label,
-  left,
-  right,
-  onChange,
-  labelGroup,
-}: SelectableBtnProps) {
-  const pal = usePalette('default')
-  const palPrimary = usePalette('inverted')
-  const {_} = useLingui()
-
-  return (
-    <Pressable
-      style={[
-        styles.selectableBtn,
-        left && styles.selectableBtnLeft,
-        right && styles.selectableBtnRight,
-        pal.border,
-        current === value ? palPrimary.view : pal.view,
-      ]}
-      onPress={() => onChange(value)}
-      accessibilityRole="button"
-      accessibilityLabel={value}
-      accessibilityHint={_(
-        msg`Set ${value} for ${labelGroup} content moderation policy`,
-      )}>
-      <Text style={current === value ? palPrimary.text : pal.text}>
-        {label}
-      </Text>
-    </Pressable>
-  )
-}
-
-const styles = StyleSheet.create({
-  container: {
-    flex: 1,
-  },
-  title: {
-    textAlign: 'center',
-    fontWeight: 'bold',
-    fontSize: 24,
-    marginBottom: 12,
-  },
-  description: {
-    paddingHorizontal: 2,
-    marginBottom: 10,
-  },
-  scrollContainer: {
-    flex: 1,
-    paddingHorizontal: 10,
-  },
-  btnContainer: {
-    paddingTop: 10,
-    paddingHorizontal: 10,
-  },
-  btnContainerMobile: {
-    paddingBottom: 40,
-    borderTopWidth: 1,
-  },
-
-  agePrompt: {
-    flexDirection: 'row',
-    justifyContent: 'space-between',
-    alignItems: 'center',
-    paddingLeft: 14,
-    paddingRight: 10,
-    paddingVertical: 8,
-    borderRadius: 8,
-  },
-
-  contentLabelPref: {
-    flexDirection: 'row',
-    justifyContent: 'space-between',
-    alignItems: 'center',
-    paddingTop: 14,
-    paddingLeft: 4,
-    marginBottom: 14,
-    borderTopWidth: 1,
-  },
-
-  selectableBtns: {
-    flexDirection: 'row',
-    marginLeft: 10,
-  },
-  selectableBtn: {
-    flexDirection: 'row',
-    justifyContent: 'center',
-    borderWidth: 1,
-    borderLeftWidth: 0,
-    paddingHorizontal: 10,
-    paddingVertical: 10,
-  },
-  selectableBtnLeft: {
-    borderTopLeftRadius: 8,
-    borderBottomLeftRadius: 8,
-    borderLeftWidth: 1,
-  },
-  selectableBtnRight: {
-    borderTopRightRadius: 8,
-    borderBottomRightRadius: 8,
-  },
-
-  btn: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    justifyContent: 'center',
-    width: '100%',
-    borderRadius: 32,
-    padding: 14,
-    backgroundColor: colors.gray1,
-  },
-  toggleBtn: {
-    paddingHorizontal: 0,
-  },
-})
diff --git a/src/view/com/modals/DeleteAccount.tsx b/src/view/com/modals/DeleteAccount.tsx
index 40d78cfe0..a355a3f42 100644
--- a/src/view/com/modals/DeleteAccount.tsx
+++ b/src/view/com/modals/DeleteAccount.tsx
@@ -1,27 +1,28 @@
 import React from 'react'
 import {
-  SafeAreaView,
   ActivityIndicator,
+  SafeAreaView,
   StyleSheet,
   TouchableOpacity,
   View,
 } from 'react-native'
-import {TextInput, ScrollView} from './util'
 import LinearGradient from 'react-native-linear-gradient'
-import * as Toast from '../util/Toast'
-import {Text} from '../util/text/Text'
-import {s, colors, gradients} from 'lib/styles'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {useModalControls} from '#/state/modals'
+import {getAgent, useSession, useSessionApi} from '#/state/session'
 import {usePalette} from 'lib/hooks/usePalette'
-import {useTheme} from 'lib/ThemeContext'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {ErrorMessage} from '../util/error/ErrorMessage'
 import {cleanError} from 'lib/strings/errors'
-import {resetToTab} from '../../../Navigation'
-import {Trans, msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {useModalControls} from '#/state/modals'
-import {useSession, useSessionApi, getAgent} from '#/state/session'
+import {colors, gradients, s} from 'lib/styles'
+import {useTheme} from 'lib/ThemeContext'
 import {isAndroid} from 'platform/detection'
+import {resetToTab} from '../../../Navigation'
+import {ErrorMessage} from '../util/error/ErrorMessage'
+import {Text} from '../util/text/Text'
+import * as Toast from '../util/Toast'
+import {ScrollView, TextInput} from './util'
 
 export const snapPoints = isAndroid ? ['90%'] : ['55%']
 
@@ -79,9 +80,7 @@ export function Component({}: {}) {
   }
   return (
     <SafeAreaView style={[s.flex1]}>
-      <ScrollView
-        contentContainerStyle={[pal.view]}
-        keyboardShouldPersistTaps="handled">
+      <ScrollView style={[pal.view]} keyboardShouldPersistTaps="handled">
         <View style={[styles.titleContainer, pal.view]}>
           <Text type="title-xl" style={[s.textCenter, pal.text]}>
             <Trans>Delete Account</Trans>
@@ -173,7 +172,7 @@ export function Component({}: {}) {
             </Text>
             <TextInput
               style={[styles.textInput, pal.borderDark, pal.text, styles.mb20]}
-              placeholder="Confirmation code"
+              placeholder={_(msg`Confirmation code`)}
               placeholderTextColor={pal.textLight.color}
               keyboardAppearance={theme.colorScheme}
               value={confirmCode}
@@ -192,7 +191,7 @@ export function Component({}: {}) {
             </Text>
             <TextInput
               style={[styles.textInput, pal.borderDark, pal.text]}
-              placeholder="Password"
+              placeholder={_(msg`Password`)}
               placeholderTextColor={pal.textLight.color}
               keyboardAppearance={theme.colorScheme}
               secureTextEntry
@@ -228,7 +227,7 @@ export function Component({}: {}) {
                   onPress={onCancel}
                   accessibilityRole="button"
                   accessibilityLabel={_(msg`Cancel account deletion`)}
-                  accessibilityHint="Exits account deletion process"
+                  accessibilityHint={_(msg`Exits account deletion process`)}
                   onAccessibilityEscape={onCancel}>
                   <Text type="button-lg" style={pal.textLight}>
                     <Trans context="action">Cancel</Trans>
diff --git a/src/view/com/modals/InAppBrowserConsent.tsx b/src/view/com/modals/InAppBrowserConsent.tsx
index 86bb46ca8..3fa515934 100644
--- a/src/view/com/modals/InAppBrowserConsent.tsx
+++ b/src/view/com/modals/InAppBrowserConsent.tsx
@@ -77,7 +77,7 @@ export function Component({href}: {href: string}) {
           }}
           accessibilityLabel={_(msg`Cancel`)}
           accessibilityHint=""
-          label="Cancel"
+          label={_(msg`Cancel`)}
           labelContainerStyle={{justifyContent: 'center', padding: 8}}
           labelStyle={[s.f18]}
         />
diff --git a/src/view/com/modals/LinkWarning.tsx b/src/view/com/modals/LinkWarning.tsx
index 81fdc7285..b5ff6700d 100644
--- a/src/view/com/modals/LinkWarning.tsx
+++ b/src/view/com/modals/LinkWarning.tsx
@@ -73,8 +73,8 @@ export function Component({text, href}: {text: string; href: string}) {
             type="primary"
             onPress={onPressVisit}
             accessibilityLabel={_(msg`Visit Site`)}
-            accessibilityHint=""
-            label="Visit Site"
+            accessibilityHint={_(msg`Opens the linked website`)}
+            label={_(msg`Visit Site`)}
             labelContainerStyle={{justifyContent: 'center', padding: 4}}
             labelStyle={[s.f18]}
           />
@@ -85,8 +85,8 @@ export function Component({text, href}: {text: string; href: string}) {
               closeModal()
             }}
             accessibilityLabel={_(msg`Cancel`)}
-            accessibilityHint=""
-            label="Cancel"
+            accessibilityHint={_(msg`Cancels opening the linked website`)}
+            label={_(msg`Cancel`)}
             labelContainerStyle={{justifyContent: 'center', padding: 4}}
             labelStyle={[s.f18]}
           />
diff --git a/src/view/com/modals/ListAddRemoveUsers.tsx b/src/view/com/modals/ListAddRemoveUsers.tsx
index 27c33f806..4715348dd 100644
--- a/src/view/com/modals/ListAddRemoveUsers.tsx
+++ b/src/view/com/modals/ListAddRemoveUsers.tsx
@@ -231,7 +231,11 @@ function UserResult({
           width: 54,
           paddingLeft: 4,
         }}>
-        <UserAvatar size={40} avatar={profile.avatar} />
+        <UserAvatar
+          size={40}
+          avatar={profile.avatar}
+          type={profile.associated?.labeler ? 'labeler' : 'user'}
+        />
       </View>
       <View
         style={{
diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx
index 8da91c75c..af86f13a3 100644
--- a/src/view/com/modals/Modal.tsx
+++ b/src/view/com/modals/Modal.tsx
@@ -1,40 +1,33 @@
-import React, {useRef, useEffect} from 'react'
+import React, {useEffect, useRef} from 'react'
 import {StyleSheet} from 'react-native'
 import {SafeAreaView} from 'react-native-safe-area-context'
-import BottomSheet from '@gorhom/bottom-sheet'
-import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop'
-import {usePalette} from 'lib/hooks/usePalette'
+import BottomSheet from '@discord/bottom-sheet/src'
 
-import {useModals, useModalControls} from '#/state/modals'
-import * as ConfirmModal from './Confirm'
-import * as EditProfileModal from './EditProfile'
-import * as RepostModal from './Repost'
-import * as SelfLabelModal from './SelfLabel'
-import * as ThreadgateModal from './Threadgate'
-import * as CreateOrEditListModal from './CreateOrEditList'
-import * as UserAddRemoveListsModal from './UserAddRemoveLists'
-import * as ListAddUserModal from './ListAddRemoveUsers'
+import {useModalControls, useModals} from '#/state/modals'
+import {usePalette} from 'lib/hooks/usePalette'
+import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop'
+import * as AddAppPassword from './AddAppPasswords'
 import * as AltImageModal from './AltImage'
 import * as EditImageModal from './AltImage'
-import * as ReportModal from './report/Modal'
-import * as AppealLabelModal from './AppealLabel'
-import * as DeleteAccountModal from './DeleteAccount'
+import * as ChangeEmailModal from './ChangeEmail'
 import * as ChangeHandleModal from './ChangeHandle'
-import * as WaitlistModal from './Waitlist'
+import * as ChangePasswordModal from './ChangePassword'
+import * as CreateOrEditListModal from './CreateOrEditList'
+import * as DeleteAccountModal from './DeleteAccount'
+import * as EditProfileModal from './EditProfile'
+import * as EmbedConsentModal from './EmbedConsent'
+import * as InAppBrowserConsentModal from './InAppBrowserConsent'
 import * as InviteCodesModal from './InviteCodes'
-import * as AddAppPassword from './AddAppPasswords'
-import * as ContentFilteringSettingsModal from './ContentFilteringSettings'
 import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
 import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
-import * as ModerationDetailsModal from './ModerationDetails'
-import * as BirthDateSettingsModal from './BirthDateSettings'
-import * as VerifyEmailModal from './VerifyEmail'
-import * as ChangeEmailModal from './ChangeEmail'
-import * as ChangePasswordModal from './ChangePassword'
-import * as SwitchAccountModal from './SwitchAccount'
 import * as LinkWarningModal from './LinkWarning'
-import * as EmbedConsentModal from './EmbedConsent'
-import * as InAppBrowserConsentModal from './InAppBrowserConsent'
+import * as ListAddUserModal from './ListAddRemoveUsers'
+import * as RepostModal from './Repost'
+import * as SelfLabelModal from './SelfLabel'
+import * as SwitchAccountModal from './SwitchAccount'
+import * as ThreadgateModal from './Threadgate'
+import * as UserAddRemoveListsModal from './UserAddRemoveLists'
+import * as VerifyEmailModal from './VerifyEmail'
 
 const DEFAULT_SNAPPOINTS = ['90%']
 const HANDLE_HEIGHT = 24
@@ -67,18 +60,9 @@ export function ModalsContainer() {
 
   let snapPoints: (string | number)[] = DEFAULT_SNAPPOINTS
   let element
-  if (activeModal?.name === 'confirm') {
-    snapPoints = ConfirmModal.snapPoints
-    element = <ConfirmModal.Component {...activeModal} />
-  } else if (activeModal?.name === 'edit-profile') {
+  if (activeModal?.name === 'edit-profile') {
     snapPoints = EditProfileModal.snapPoints
     element = <EditProfileModal.Component {...activeModal} />
-  } else if (activeModal?.name === 'report') {
-    snapPoints = ReportModal.snapPoints
-    element = <ReportModal.Component {...activeModal} />
-  } else if (activeModal?.name === 'appeal-label') {
-    snapPoints = AppealLabelModal.snapPoints
-    element = <AppealLabelModal.Component {...activeModal} />
   } else if (activeModal?.name === 'create-or-edit-list') {
     snapPoints = CreateOrEditListModal.snapPoints
     element = <CreateOrEditListModal.Component {...activeModal} />
@@ -109,30 +93,18 @@ export function ModalsContainer() {
   } else if (activeModal?.name === 'change-handle') {
     snapPoints = ChangeHandleModal.snapPoints
     element = <ChangeHandleModal.Component {...activeModal} />
-  } else if (activeModal?.name === 'waitlist') {
-    snapPoints = WaitlistModal.snapPoints
-    element = <WaitlistModal.Component />
   } else if (activeModal?.name === 'invite-codes') {
     snapPoints = InviteCodesModal.snapPoints
     element = <InviteCodesModal.Component />
   } else if (activeModal?.name === 'add-app-password') {
     snapPoints = AddAppPassword.snapPoints
     element = <AddAppPassword.Component />
-  } else if (activeModal?.name === 'content-filtering-settings') {
-    snapPoints = ContentFilteringSettingsModal.snapPoints
-    element = <ContentFilteringSettingsModal.Component />
   } else if (activeModal?.name === 'content-languages-settings') {
     snapPoints = ContentLanguagesSettingsModal.snapPoints
     element = <ContentLanguagesSettingsModal.Component />
   } else if (activeModal?.name === 'post-languages-settings') {
     snapPoints = PostLanguagesSettingsModal.snapPoints
     element = <PostLanguagesSettingsModal.Component />
-  } else if (activeModal?.name === 'moderation-details') {
-    snapPoints = ModerationDetailsModal.snapPoints
-    element = <ModerationDetailsModal.Component {...activeModal} />
-  } else if (activeModal?.name === 'birth-date-settings') {
-    snapPoints = BirthDateSettingsModal.snapPoints
-    element = <BirthDateSettingsModal.Component />
   } else if (activeModal?.name === 'verify-email') {
     snapPoints = VerifyEmailModal.snapPoints
     element = <VerifyEmailModal.Component {...activeModal} />
diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx
index 97a60be91..7e5d548ac 100644
--- a/src/view/com/modals/Modal.web.tsx
+++ b/src/view/com/modals/Modal.web.tsx
@@ -7,10 +7,7 @@ import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock'
 
 import {useModals, useModalControls} from '#/state/modals'
 import type {Modal as ModalIface} from '#/state/modals'
-import * as ConfirmModal from './Confirm'
 import * as EditProfileModal from './EditProfile'
-import * as ReportModal from './report/Modal'
-import * as AppealLabelModal from './AppealLabel'
 import * as CreateOrEditListModal from './CreateOrEditList'
 import * as UserAddRemoveLists from './UserAddRemoveLists'
 import * as ListAddUserModal from './ListAddRemoveUsers'
@@ -22,14 +19,10 @@ import * as CropImageModal from './crop-image/CropImage.web'
 import * as AltTextImageModal from './AltImage'
 import * as EditImageModal from './EditImage'
 import * as ChangeHandleModal from './ChangeHandle'
-import * as WaitlistModal from './Waitlist'
 import * as InviteCodesModal from './InviteCodes'
 import * as AddAppPassword from './AddAppPasswords'
-import * as ContentFilteringSettingsModal from './ContentFilteringSettings'
 import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
 import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
-import * as ModerationDetailsModal from './ModerationDetails'
-import * as BirthDateSettingsModal from './BirthDateSettings'
 import * as VerifyEmailModal from './VerifyEmail'
 import * as ChangeEmailModal from './ChangeEmail'
 import * as ChangePasswordModal from './ChangePassword'
@@ -79,14 +72,8 @@ function Modal({modal}: {modal: ModalIface}) {
   }
 
   let element
-  if (modal.name === 'confirm') {
-    element = <ConfirmModal.Component {...modal} />
-  } else if (modal.name === 'edit-profile') {
+  if (modal.name === 'edit-profile') {
     element = <EditProfileModal.Component {...modal} />
-  } else if (modal.name === 'report') {
-    element = <ReportModal.Component {...modal} />
-  } else if (modal.name === 'appeal-label') {
-    element = <AppealLabelModal.Component {...modal} />
   } else if (modal.name === 'create-or-edit-list') {
     element = <CreateOrEditListModal.Component {...modal} />
   } else if (modal.name === 'user-add-remove-lists') {
@@ -105,14 +92,10 @@ function Modal({modal}: {modal: ModalIface}) {
     element = <ThreadgateModal.Component {...modal} />
   } else if (modal.name === 'change-handle') {
     element = <ChangeHandleModal.Component {...modal} />
-  } else if (modal.name === 'waitlist') {
-    element = <WaitlistModal.Component />
   } else if (modal.name === 'invite-codes') {
     element = <InviteCodesModal.Component />
   } else if (modal.name === 'add-app-password') {
     element = <AddAppPassword.Component />
-  } else if (modal.name === 'content-filtering-settings') {
-    element = <ContentFilteringSettingsModal.Component />
   } else if (modal.name === 'content-languages-settings') {
     element = <ContentLanguagesSettingsModal.Component />
   } else if (modal.name === 'post-languages-settings') {
@@ -121,10 +104,6 @@ function Modal({modal}: {modal: ModalIface}) {
     element = <AltTextImageModal.Component {...modal} />
   } else if (modal.name === 'edit-image') {
     element = <EditImageModal.Component {...modal} />
-  } else if (modal.name === 'moderation-details') {
-    element = <ModerationDetailsModal.Component {...modal} />
-  } else if (modal.name === 'birth-date-settings') {
-    element = <BirthDateSettingsModal.Component />
   } else if (modal.name === 'verify-email') {
     element = <VerifyEmailModal.Component {...modal} />
   } else if (modal.name === 'change-email') {
diff --git a/src/view/com/modals/ModerationDetails.tsx b/src/view/com/modals/ModerationDetails.tsx
deleted file mode 100644
index f890d50dc..000000000
--- a/src/view/com/modals/ModerationDetails.tsx
+++ /dev/null
@@ -1,142 +0,0 @@
-import React from 'react'
-import {StyleSheet, View} from 'react-native'
-import {ModerationUI} from '@atproto/api'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {s} from 'lib/styles'
-import {Text} from '../util/text/Text'
-import {TextLink} from '../util/Link'
-import {usePalette} from 'lib/hooks/usePalette'
-import {isWeb} from 'platform/detection'
-import {listUriToHref} from 'lib/strings/url-helpers'
-import {Button} from '../util/forms/Button'
-import {useModalControls} from '#/state/modals'
-import {useLingui} from '@lingui/react'
-import {Trans, msg} from '@lingui/macro'
-
-export const snapPoints = [300]
-
-export function Component({
-  context,
-  moderation,
-}: {
-  context: 'account' | 'content'
-  moderation: ModerationUI
-}) {
-  const {closeModal} = useModalControls()
-  const {isMobile} = useWebMediaQueries()
-  const pal = usePalette('default')
-  const {_} = useLingui()
-
-  let name
-  let description
-  if (!moderation.cause) {
-    name = _(msg`Content Warning`)
-    description = _(
-      msg`Moderator has chosen to set a general warning on the content.`,
-    )
-  } else if (moderation.cause.type === 'blocking') {
-    if (moderation.cause.source.type === 'list') {
-      const list = moderation.cause.source.list
-      name = _(msg`User Blocked by List`)
-      description = (
-        <Trans>
-          This user is included in the{' '}
-          <TextLink
-            type="2xl"
-            href={listUriToHref(list.uri)}
-            text={list.name}
-            style={pal.link}
-          />{' '}
-          list which you have blocked.
-        </Trans>
-      )
-    } else {
-      name = _(msg`User Blocked`)
-      description = _(
-        msg`You have blocked this user. You cannot view their content.`,
-      )
-    }
-  } else if (moderation.cause.type === 'blocked-by') {
-    name = _(msg`User Blocks You`)
-    description = _(
-      msg`This user has blocked you. You cannot view their content.`,
-    )
-  } else if (moderation.cause.type === 'block-other') {
-    name = _(msg`Content Not Available`)
-    description = _(
-      msg`This content is not available because one of the users involved has blocked the other.`,
-    )
-  } else if (moderation.cause.type === 'muted') {
-    if (moderation.cause.source.type === 'list') {
-      const list = moderation.cause.source.list
-      name = _(msg`Account Muted by List`)
-      description = (
-        <Trans>
-          This user is included in the{' '}
-          <TextLink
-            type="2xl"
-            href={listUriToHref(list.uri)}
-            text={list.name}
-            style={pal.link}
-          />{' '}
-          list which you have muted.
-        </Trans>
-      )
-    } else {
-      name = _(msg`Account Muted`)
-      description = _(msg`You have muted this user.`)
-    }
-  } else {
-    name = moderation.cause.labelDef.strings[context].en.name
-    description = moderation.cause.labelDef.strings[context].en.description
-  }
-
-  return (
-    <View
-      testID="moderationDetailsModal"
-      style={[
-        styles.container,
-        {
-          paddingHorizontal: isMobile ? 14 : 0,
-        },
-        pal.view,
-      ]}>
-      <Text type="title-xl" style={[pal.text, styles.title]}>
-        {name}
-      </Text>
-      <Text type="2xl" style={[pal.text, styles.description]}>
-        {description}
-      </Text>
-      <View style={s.flex1} />
-      <Button
-        type="primary"
-        style={styles.btn}
-        onPress={() => {
-          closeModal()
-        }}>
-        <Text type="button-lg" style={[pal.textLight, s.textCenter, s.white]}>
-          Okay
-        </Text>
-      </Button>
-    </View>
-  )
-}
-
-const styles = StyleSheet.create({
-  container: {
-    flex: 1,
-  },
-  title: {
-    textAlign: 'center',
-    fontWeight: 'bold',
-    marginBottom: 12,
-  },
-  description: {
-    textAlign: 'center',
-  },
-  btn: {
-    paddingVertical: 14,
-    marginTop: isWeb ? 40 : 0,
-    marginBottom: isWeb ? 0 : 40,
-  },
-})
diff --git a/src/view/com/modals/SwitchAccount.tsx b/src/view/com/modals/SwitchAccount.tsx
index c034c4b52..03bef719e 100644
--- a/src/view/com/modals/SwitchAccount.tsx
+++ b/src/view/com/modals/SwitchAccount.tsx
@@ -5,22 +5,23 @@ import {
   TouchableOpacity,
   View,
 } from 'react-native'
-import {Text} from '../util/text/Text'
-import {s} from 'lib/styles'
-import {usePalette} from 'lib/hooks/usePalette'
+import {BottomSheetScrollView} from '@discord/bottom-sheet/src'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {useProfileQuery} from '#/state/queries/profile'
+import {SessionAccount, useSession, useSessionApi} from '#/state/session'
+import {useCloseAllActiveElements} from '#/state/util'
 import {useAnalytics} from 'lib/analytics/analytics'
+import {Haptics} from 'lib/haptics'
 import {useAccountSwitcher} from 'lib/hooks/useAccountSwitcher'
-import {UserAvatar} from '../util/UserAvatar'
+import {usePalette} from 'lib/hooks/usePalette'
+import {makeProfileLink} from 'lib/routes/links'
+import {s} from 'lib/styles'
 import {AccountDropdownBtn} from '../util/AccountDropdownBtn'
 import {Link} from '../util/Link'
-import {makeProfileLink} from 'lib/routes/links'
-import {BottomSheetScrollView} from '@gorhom/bottom-sheet'
-import {Haptics} from 'lib/haptics'
-import {Trans, msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {useSession, useSessionApi, SessionAccount} from '#/state/session'
-import {useProfileQuery} from '#/state/queries/profile'
-import {useCloseAllActiveElements} from '#/state/util'
+import {Text} from '../util/text/Text'
+import {UserAvatar} from '../util/UserAvatar'
 
 export const snapPoints = ['40%', '90%']
 
@@ -39,13 +40,17 @@ function SwitchAccountCard({account}: {account: SessionAccount}) {
     track('Settings:SignOutButtonClicked')
     closeAllActiveElements()
     // needs to be in timeout or the modal re-opens
-    setTimeout(() => logout(), 0)
+    setTimeout(() => logout('SwitchAccount'), 0)
   }, [track, logout, closeAllActiveElements])
 
   const contents = (
     <View style={[pal.view, styles.linkCard]}>
       <View style={styles.avi}>
-        <UserAvatar size={40} avatar={profile?.avatar} />
+        <UserAvatar
+          size={40}
+          avatar={profile?.avatar}
+          type={profile?.associated?.labeler ? 'labeler' : 'user'}
+        />
       </View>
       <View style={[s.flex1]}>
         <Text type="md-bold" style={pal.text} numberOfLines={1}>
@@ -91,7 +96,9 @@ function SwitchAccountCard({account}: {account: SessionAccount}) {
       key={account.did}
       style={[isSwitchingAccounts && styles.dimmed]}
       onPress={
-        isSwitchingAccounts ? undefined : () => onPressSwitchAccount(account)
+        isSwitchingAccounts
+          ? undefined
+          : () => onPressSwitchAccount(account, 'SwitchAccount')
       }
       accessibilityRole="button"
       accessibilityLabel={_(msg`Switch to ${account.handle}`)}
diff --git a/src/view/com/modals/UserAddRemoveLists.tsx b/src/view/com/modals/UserAddRemoveLists.tsx
index 8452f2513..8a61b1a70 100644
--- a/src/view/com/modals/UserAddRemoveLists.tsx
+++ b/src/view/com/modals/UserAddRemoveLists.tsx
@@ -180,7 +180,7 @@ function ListItem({
         },
       ]}>
       <View style={styles.listItemAvi}>
-        <UserAvatar size={40} avatar={list.avatar} />
+        <UserAvatar size={40} avatar={list.avatar} type="list" />
       </View>
       <View style={styles.listItemContent}>
         <Text
diff --git a/src/view/com/modals/VerifyEmail.tsx b/src/view/com/modals/VerifyEmail.tsx
index 30a57afc5..d3086d383 100644
--- a/src/view/com/modals/VerifyEmail.tsx
+++ b/src/view/com/modals/VerifyEmail.tsx
@@ -149,7 +149,7 @@ export function Component({showReminder}: {showReminder?: boolean}) {
               onPress={onEmailIncorrect}
               style={styles.changeEmailLink}>
               <Text type="lg" style={pal.link}>
-                Change
+                <Trans>Change</Trans>
               </Text>
             </Pressable>
           </>
diff --git a/src/view/com/modals/Waitlist.tsx b/src/view/com/modals/Waitlist.tsx
deleted file mode 100644
index 263dd27a2..000000000
--- a/src/view/com/modals/Waitlist.tsx
+++ /dev/null
@@ -1,190 +0,0 @@
-import React from 'react'
-import {
-  ActivityIndicator,
-  StyleSheet,
-  TouchableOpacity,
-  View,
-} from 'react-native'
-import {TextInput} from './util'
-import {
-  FontAwesomeIcon,
-  FontAwesomeIconStyle,
-} from '@fortawesome/react-native-fontawesome'
-import LinearGradient from 'react-native-linear-gradient'
-import {Text} from '../util/text/Text'
-import {s, gradients} from 'lib/styles'
-import {usePalette} from 'lib/hooks/usePalette'
-import {useTheme} from 'lib/ThemeContext'
-import {ErrorMessage} from '../util/error/ErrorMessage'
-import {cleanError} from 'lib/strings/errors'
-import {Trans, msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {useModalControls} from '#/state/modals'
-
-export const snapPoints = ['80%']
-
-export function Component({}: {}) {
-  const pal = usePalette('default')
-  const theme = useTheme()
-  const {_} = useLingui()
-  const {closeModal} = useModalControls()
-  const [email, setEmail] = React.useState<string>('')
-  const [isEmailSent, setIsEmailSent] = React.useState<boolean>(false)
-  const [isProcessing, setIsProcessing] = React.useState<boolean>(false)
-  const [error, setError] = React.useState<string>('')
-
-  const onPressSignup = async () => {
-    setError('')
-    setIsProcessing(true)
-    try {
-      const res = await fetch('https://bsky.app/api/waitlist', {
-        method: 'POST',
-        headers: {'Content-Type': 'application/json'},
-        body: JSON.stringify({email}),
-      })
-      const resBody = await res.json()
-      if (resBody.success) {
-        setIsEmailSent(true)
-      } else {
-        setError(
-          resBody.error ||
-            _(msg`Something went wrong. Check your email and try again.`),
-        )
-      }
-    } catch (e: any) {
-      setError(cleanError(e))
-    }
-    setIsProcessing(false)
-  }
-  const onCancel = () => {
-    closeModal()
-  }
-
-  return (
-    <View style={[styles.container, pal.view]}>
-      <View style={[styles.innerContainer, pal.view]}>
-        <Text type="title-xl" style={[styles.title, pal.text]}>
-          <Trans>Join the waitlist</Trans>
-        </Text>
-        <Text type="lg" style={[styles.description, pal.text]}>
-          <Trans>
-            Bluesky uses invites to build a healthier community. If you don't
-            know anybody with an invite, you can sign up for the waitlist and
-            we'll send one soon.
-          </Trans>
-        </Text>
-        <TextInput
-          style={[styles.textInput, pal.borderDark, pal.text, s.mb10, s.mt10]}
-          placeholder={_(msg`Enter your email`)}
-          placeholderTextColor={pal.textLight.color}
-          autoCapitalize="none"
-          autoCorrect={false}
-          keyboardAppearance={theme.colorScheme}
-          value={email}
-          onChangeText={setEmail}
-          onSubmitEditing={onPressSignup}
-          enterKeyHint="done"
-          accessible={true}
-          accessibilityLabel={_(msg`Email`)}
-          accessibilityHint={_(
-            msg`Input your email to get on the Bluesky waitlist`,
-          )}
-        />
-        {error ? (
-          <View style={s.mt10}>
-            <ErrorMessage message={error} style={styles.error} />
-          </View>
-        ) : undefined}
-        {isProcessing ? (
-          <View style={[styles.btn, s.mt10]}>
-            <ActivityIndicator />
-          </View>
-        ) : isEmailSent ? (
-          <View style={[styles.btn, s.mt10]}>
-            <FontAwesomeIcon
-              icon="check"
-              style={pal.text as FontAwesomeIconStyle}
-            />
-            <Text style={[s.ml10, pal.text]}>
-              <Trans>
-                Your email has been saved! We&apos;ll be in touch soon.
-              </Trans>
-            </Text>
-          </View>
-        ) : (
-          <>
-            <TouchableOpacity
-              onPress={onPressSignup}
-              accessibilityRole="button"
-              accessibilityHint={_(
-                msg`Confirms signing up ${email} to the waitlist`,
-              )}>
-              <LinearGradient
-                colors={[gradients.blueLight.start, gradients.blueLight.end]}
-                start={{x: 0, y: 0}}
-                end={{x: 1, y: 1}}
-                style={[styles.btn]}>
-                <Text type="button-lg" style={[s.white, s.bold]}>
-                  <Trans>Join Waitlist</Trans>
-                </Text>
-              </LinearGradient>
-            </TouchableOpacity>
-            <TouchableOpacity
-              style={[styles.btn, s.mt10]}
-              onPress={onCancel}
-              accessibilityRole="button"
-              accessibilityLabel={_(msg`Cancel waitlist signup`)}
-              accessibilityHint={_(
-                msg`Exits signing up for waitlist with ${email}`,
-              )}
-              onAccessibilityEscape={onCancel}>
-              <Text type="button-lg" style={pal.textLight}>
-                <Trans>Cancel</Trans>
-              </Text>
-            </TouchableOpacity>
-          </>
-        )}
-      </View>
-    </View>
-  )
-}
-
-const styles = StyleSheet.create({
-  container: {
-    flex: 1,
-  },
-  innerContainer: {
-    paddingBottom: 20,
-  },
-  title: {
-    textAlign: 'center',
-    marginTop: 12,
-    marginBottom: 12,
-  },
-  description: {
-    textAlign: 'center',
-    paddingHorizontal: 22,
-    marginBottom: 10,
-  },
-  textInput: {
-    borderWidth: 1,
-    borderRadius: 6,
-    paddingHorizontal: 16,
-    paddingVertical: 12,
-    fontSize: 20,
-    marginHorizontal: 20,
-  },
-  btn: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    justifyContent: 'center',
-    borderRadius: 32,
-    padding: 14,
-    marginHorizontal: 20,
-  },
-  error: {
-    borderRadius: 6,
-    marginHorizontal: 20,
-    marginBottom: 20,
-  },
-})
diff --git a/src/view/com/modals/crop-image/CropImage.web.tsx b/src/view/com/modals/crop-image/CropImage.web.tsx
index 6f094a1fd..98a2494ed 100644
--- a/src/view/com/modals/crop-image/CropImage.web.tsx
+++ b/src/view/com/modals/crop-image/CropImage.web.tsx
@@ -100,7 +100,7 @@ export function Component({
           onPress={doSetAs(AspectRatio.Wide)}
           accessibilityRole="button"
           accessibilityLabel={_(msg`Wide`)}
-          accessibilityHint="Sets image aspect ratio to wide">
+          accessibilityHint={_(msg`Sets image aspect ratio to wide`)}>
           <RectWideIcon
             size={24}
             style={as === AspectRatio.Wide ? s.blue3 : pal.text}
@@ -110,7 +110,7 @@ export function Component({
           onPress={doSetAs(AspectRatio.Tall)}
           accessibilityRole="button"
           accessibilityLabel={_(msg`Tall`)}
-          accessibilityHint="Sets image aspect ratio to tall">
+          accessibilityHint={_(msg`Sets image aspect ratio to tall`)}>
           <RectTallIcon
             size={24}
             style={as === AspectRatio.Tall ? s.blue3 : pal.text}
@@ -120,7 +120,7 @@ export function Component({
           onPress={doSetAs(AspectRatio.Square)}
           accessibilityRole="button"
           accessibilityLabel={_(msg`Square`)}
-          accessibilityHint="Sets image aspect ratio to square">
+          accessibilityHint={_(msg`Sets image aspect ratio to square`)}>
           <SquareIcon
             size={24}
             style={as === AspectRatio.Square ? s.blue3 : pal.text}
@@ -132,9 +132,9 @@ export function Component({
           onPress={onPressCancel}
           accessibilityRole="button"
           accessibilityLabel={_(msg`Cancel image crop`)}
-          accessibilityHint="Exits image cropping process">
+          accessibilityHint={_(msg`Exits image cropping process`)}>
           <Text type="xl" style={pal.link}>
-            Cancel
+            <Trans>Cancel</Trans>
           </Text>
         </TouchableOpacity>
         <View style={s.flex1} />
@@ -142,7 +142,7 @@ export function Component({
           onPress={onPressDone}
           accessibilityRole="button"
           accessibilityLabel={_(msg`Save image crop`)}
-          accessibilityHint="Saves image crop settings">
+          accessibilityHint={_(msg`Saves image crop settings`)}>
           <LinearGradient
             colors={[gradients.blueLight.start, gradients.blueLight.end]}
             start={{x: 0, y: 0}}
diff --git a/src/view/com/modals/report/InputIssueDetails.tsx b/src/view/com/modals/report/InputIssueDetails.tsx
deleted file mode 100644
index 2bc86f75e..000000000
--- a/src/view/com/modals/report/InputIssueDetails.tsx
+++ /dev/null
@@ -1,100 +0,0 @@
-import React from 'react'
-import {View, TouchableOpacity, StyleSheet} from 'react-native'
-import {TextInput} from '../util'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {CharProgress} from '../../composer/char-progress/CharProgress'
-import {Text} from '../../util/text/Text'
-import {usePalette} from 'lib/hooks/usePalette'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {s} from 'lib/styles'
-import {SendReportButton} from './SendReportButton'
-import {Trans, msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-
-export function InputIssueDetails({
-  details,
-  setDetails,
-  goBack,
-  submitReport,
-  isProcessing,
-}: {
-  details: string | undefined
-  setDetails: (v: string) => void
-  goBack: () => void
-  submitReport: () => void
-  isProcessing: boolean
-}) {
-  const pal = usePalette('default')
-  const {_} = useLingui()
-  const {isMobile} = useWebMediaQueries()
-
-  return (
-    <View
-      style={{
-        marginTop: isMobile ? 12 : 0,
-      }}>
-      <TouchableOpacity
-        testID="addDetailsBtn"
-        style={[s.mb10, styles.backBtn]}
-        onPress={goBack}
-        accessibilityRole="button"
-        accessibilityLabel={_(msg`Add details`)}
-        accessibilityHint="Add more details to your report">
-        <FontAwesomeIcon size={18} icon="angle-left" style={[pal.link]} />
-        <Text style={[pal.text, s.f18, pal.link]}>
-          {' '}
-          <Trans>Back</Trans>
-        </Text>
-      </TouchableOpacity>
-      <View style={[pal.btn, styles.detailsInputContainer]}>
-        <TextInput
-          accessibilityLabel={_(msg`Text input field`)}
-          accessibilityHint="Enter a reason for reporting this post."
-          placeholder="Enter a reason or any other details here."
-          placeholderTextColor={pal.textLight.color}
-          value={details}
-          onChangeText={setDetails}
-          autoFocus={true}
-          numberOfLines={3}
-          multiline={true}
-          textAlignVertical="top"
-          maxLength={300}
-          style={[styles.detailsInput, pal.text]}
-        />
-        <View style={styles.detailsInputBottomBar}>
-          <View style={styles.charCounter}>
-            <CharProgress count={details?.length || 0} />
-          </View>
-        </View>
-      </View>
-      <SendReportButton onPress={submitReport} isProcessing={isProcessing} />
-    </View>
-  )
-}
-
-const styles = StyleSheet.create({
-  backBtn: {
-    flexDirection: 'row',
-    alignItems: 'center',
-  },
-  detailsInputContainer: {
-    borderRadius: 8,
-  },
-  detailsInput: {
-    paddingHorizontal: 12,
-    paddingTop: 12,
-    paddingBottom: 12,
-    borderRadius: 8,
-    minHeight: 100,
-    fontSize: 16,
-  },
-  detailsInputBottomBar: {
-    alignSelf: 'flex-end',
-  },
-  charCounter: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    paddingRight: 10,
-    paddingBottom: 8,
-  },
-})
diff --git a/src/view/com/modals/report/Modal.tsx b/src/view/com/modals/report/Modal.tsx
deleted file mode 100644
index abbad9b40..000000000
--- a/src/view/com/modals/report/Modal.tsx
+++ /dev/null
@@ -1,223 +0,0 @@
-import React, {useState, useMemo} from 'react'
-import {Linking, StyleSheet, TouchableOpacity, View} from 'react-native'
-import {ScrollView} from 'react-native-gesture-handler'
-import {AtUri} from '@atproto/api'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {s} from 'lib/styles'
-import {Text} from '../../util/text/Text'
-import * as Toast from '../../util/Toast'
-import {ErrorMessage} from '../../util/error/ErrorMessage'
-import {cleanError} from 'lib/strings/errors'
-import {usePalette} from 'lib/hooks/usePalette'
-import {SendReportButton} from './SendReportButton'
-import {InputIssueDetails} from './InputIssueDetails'
-import {ReportReasonOptions} from './ReasonOptions'
-import {CollectionId} from './types'
-import {Trans, msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {useModalControls} from '#/state/modals'
-import {getAgent} from '#/state/session'
-
-const DMCA_LINK = 'https://bsky.social/about/support/copyright'
-
-export const snapPoints = [575]
-
-const CollectionNames = {
-  [CollectionId.FeedGenerator]: 'Feed',
-  [CollectionId.Profile]: 'Profile',
-  [CollectionId.List]: 'List',
-  [CollectionId.Post]: 'Post',
-}
-
-type ReportComponentProps =
-  | {
-      uri: string
-      cid: string
-    }
-  | {
-      did: string
-    }
-
-export function Component(content: ReportComponentProps) {
-  const {closeModal} = useModalControls()
-  const pal = usePalette('default')
-  const {isMobile} = useWebMediaQueries()
-  const [isProcessing, setIsProcessing] = useState(false)
-  const [showDetailsInput, setShowDetailsInput] = useState(false)
-  const [error, setError] = useState<string>('')
-  const [issue, setIssue] = useState<string>('')
-  const [details, setDetails] = useState<string>('')
-  const isAccountReport = 'did' in content
-  const subjectKey = isAccountReport ? content.did : content.uri
-  const atUri = useMemo(
-    () => (!isAccountReport ? new AtUri(subjectKey) : null),
-    [isAccountReport, subjectKey],
-  )
-
-  const submitReport = async () => {
-    setError('')
-    if (!issue) {
-      return
-    }
-    setIsProcessing(true)
-    try {
-      if (issue === '__copyright__') {
-        Linking.openURL(DMCA_LINK)
-        closeModal()
-        return
-      }
-      const $type = !isAccountReport
-        ? 'com.atproto.repo.strongRef'
-        : 'com.atproto.admin.defs#repoRef'
-      await getAgent().createModerationReport({
-        reasonType: issue,
-        subject: {
-          $type,
-          ...content,
-        },
-        reason: details,
-      })
-      Toast.show("Thank you for your report! We'll look into it promptly.")
-
-      closeModal()
-      return
-    } catch (e: any) {
-      setError(cleanError(e))
-      setIsProcessing(false)
-    }
-  }
-
-  const goBack = () => {
-    setShowDetailsInput(false)
-  }
-
-  return (
-    <ScrollView testID="reportModal" style={[s.flex1, pal.view]}>
-      <View
-        style={[
-          styles.container,
-          isMobile && {
-            paddingBottom: 40,
-          },
-        ]}>
-        {showDetailsInput ? (
-          <InputIssueDetails
-            details={details}
-            setDetails={setDetails}
-            goBack={goBack}
-            submitReport={submitReport}
-            isProcessing={isProcessing}
-          />
-        ) : (
-          <SelectIssue
-            setShowDetailsInput={setShowDetailsInput}
-            error={error}
-            issue={issue}
-            setIssue={setIssue}
-            submitReport={submitReport}
-            isProcessing={isProcessing}
-            atUri={atUri}
-          />
-        )}
-      </View>
-    </ScrollView>
-  )
-}
-
-// If no atUri is passed, that means the reporting collection is account
-const getCollectionNameForReport = (atUri: AtUri | null) => {
-  if (!atUri) return 'Account'
-  // Generic fallback for any collection being reported
-  return CollectionNames[atUri.collection as CollectionId] || 'Content'
-}
-
-const SelectIssue = ({
-  error,
-  setShowDetailsInput,
-  issue,
-  setIssue,
-  submitReport,
-  isProcessing,
-  atUri,
-}: {
-  error: string | undefined
-  setShowDetailsInput: (v: boolean) => void
-  issue: string | undefined
-  setIssue: (v: string) => void
-  submitReport: () => void
-  isProcessing: boolean
-  atUri: AtUri | null
-}) => {
-  const pal = usePalette('default')
-  const {_} = useLingui()
-  const collectionName = getCollectionNameForReport(atUri)
-  const onSelectIssue = (v: string) => setIssue(v)
-  const goToDetails = () => {
-    if (issue === '__copyright__') {
-      Linking.openURL(DMCA_LINK)
-      return
-    }
-    setShowDetailsInput(true)
-  }
-
-  return (
-    <>
-      <Text style={[pal.text, styles.title]}>
-        <Trans>Report {collectionName}</Trans>
-      </Text>
-      <Text style={[pal.textLight, styles.description]}>
-        <Trans>What is the issue with this {collectionName}?</Trans>
-      </Text>
-      <View style={{marginBottom: 10}}>
-        <ReportReasonOptions
-          atUri={atUri}
-          selectedIssue={issue}
-          onSelectIssue={onSelectIssue}
-        />
-      </View>
-      {error ? <ErrorMessage message={error} /> : undefined}
-      {/* If no atUri is present, the report would be for account in which case, we allow sending without specifying a reason */}
-      {issue || !atUri ? (
-        <>
-          <SendReportButton
-            onPress={submitReport}
-            isProcessing={isProcessing}
-          />
-          <TouchableOpacity
-            testID="addDetailsBtn"
-            style={styles.addDetailsBtn}
-            onPress={goToDetails}
-            accessibilityRole="button"
-            accessibilityLabel={_(msg`Add details`)}
-            accessibilityHint="Add more details to your report">
-            <Text style={[s.f18, pal.link]}>
-              <Trans>Add details to report</Trans>
-            </Text>
-          </TouchableOpacity>
-        </>
-      ) : undefined}
-    </>
-  )
-}
-
-const styles = StyleSheet.create({
-  container: {
-    paddingHorizontal: 10,
-  },
-  title: {
-    textAlign: 'center',
-    fontWeight: 'bold',
-    fontSize: 24,
-    marginBottom: 12,
-  },
-  description: {
-    textAlign: 'center',
-    fontSize: 17,
-    paddingHorizontal: 22,
-    marginBottom: 10,
-  },
-  addDetailsBtn: {
-    padding: 14,
-    alignSelf: 'center',
-  },
-})
diff --git a/src/view/com/modals/report/ReasonOptions.tsx b/src/view/com/modals/report/ReasonOptions.tsx
deleted file mode 100644
index 23b49b664..000000000
--- a/src/view/com/modals/report/ReasonOptions.tsx
+++ /dev/null
@@ -1,123 +0,0 @@
-import {View} from 'react-native'
-import React, {useMemo} from 'react'
-import {AtUri, ComAtprotoModerationDefs} from '@atproto/api'
-
-import {Text} from '../../util/text/Text'
-import {UsePaletteValue, usePalette} from 'lib/hooks/usePalette'
-import {RadioGroup, RadioGroupItem} from 'view/com/util/forms/RadioGroup'
-import {CollectionId} from './types'
-
-type ReasonMap = Record<string, {title: string; description: string}>
-const CommonReasons = {
-  [ComAtprotoModerationDefs.REASONRUDE]: {
-    title: 'Anti-Social Behavior',
-    description: 'Harassment, trolling, or intolerance',
-  },
-  [ComAtprotoModerationDefs.REASONVIOLATION]: {
-    title: 'Illegal and Urgent',
-    description: 'Glaring violations of law or terms of service',
-  },
-  [ComAtprotoModerationDefs.REASONOTHER]: {
-    title: 'Other',
-    description: 'An issue not included in these options',
-  },
-}
-const CollectionToReasonsMap: Record<string, ReasonMap> = {
-  [CollectionId.Post]: {
-    [ComAtprotoModerationDefs.REASONSPAM]: {
-      title: 'Spam',
-      description: 'Excessive mentions or replies',
-    },
-    [ComAtprotoModerationDefs.REASONSEXUAL]: {
-      title: 'Unwanted Sexual Content',
-      description: 'Nudity or pornography not labeled as such',
-    },
-    __copyright__: {
-      title: 'Copyright Violation',
-      description: 'Contains copyrighted material',
-    },
-    ...CommonReasons,
-  },
-  [CollectionId.List]: {
-    ...CommonReasons,
-    [ComAtprotoModerationDefs.REASONVIOLATION]: {
-      title: 'Name or Description Violates Community Standards',
-      description: 'Terms used violate community standards',
-    },
-  },
-}
-const AccountReportReasons = {
-  [ComAtprotoModerationDefs.REASONMISLEADING]: {
-    title: 'Misleading Account',
-    description: 'Impersonation or false claims about identity or affiliation',
-  },
-  [ComAtprotoModerationDefs.REASONSPAM]: {
-    title: 'Frequently Posts Unwanted Content',
-    description: 'Spam; excessive mentions or replies',
-  },
-  [ComAtprotoModerationDefs.REASONVIOLATION]: {
-    title: 'Name or Description Violates Community Standards',
-    description: 'Terms used violate community standards',
-  },
-}
-
-const Option = ({
-  pal,
-  title,
-  description,
-}: {
-  pal: UsePaletteValue
-  description: string
-  title: string
-}) => {
-  return (
-    <View>
-      <Text style={pal.text} type="md-bold">
-        {title}
-      </Text>
-      <Text style={pal.textLight}>{description}</Text>
-    </View>
-  )
-}
-
-// This is mostly just content copy without almost any logic
-// so this may grow over time and it makes sense to split it up into its own file
-// to keep it separate from the actual reporting modal logic
-const useReportRadioOptions = (pal: UsePaletteValue, atUri: AtUri | null) =>
-  useMemo(() => {
-    let items: ReasonMap = {...CommonReasons}
-    // If no atUri is passed, that means the reporting collection is account
-    if (!atUri) {
-      items = {...AccountReportReasons}
-    }
-
-    if (atUri?.collection && CollectionToReasonsMap[atUri.collection]) {
-      items = {...CollectionToReasonsMap[atUri.collection]}
-    }
-
-    return Object.entries(items).map(([key, {title, description}]) => ({
-      key,
-      label: <Option pal={pal} title={title} description={description} />,
-    }))
-  }, [pal, atUri])
-
-export const ReportReasonOptions = ({
-  atUri,
-  selectedIssue,
-  onSelectIssue,
-}: {
-  atUri: AtUri | null
-  selectedIssue?: string
-  onSelectIssue: (key: string) => void
-}) => {
-  const pal = usePalette('default')
-  const ITEMS: RadioGroupItem[] = useReportRadioOptions(pal, atUri)
-  return (
-    <RadioGroup
-      items={ITEMS}
-      onSelect={onSelectIssue}
-      testID="reportReasonRadios"
-      initialSelection={selectedIssue}
-    />
-  )
-}
diff --git a/src/view/com/modals/report/SendReportButton.tsx b/src/view/com/modals/report/SendReportButton.tsx
deleted file mode 100644
index 40c239bff..000000000
--- a/src/view/com/modals/report/SendReportButton.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-import React from 'react'
-import LinearGradient from 'react-native-linear-gradient'
-import {
-  ActivityIndicator,
-  StyleSheet,
-  TouchableOpacity,
-  View,
-} from 'react-native'
-import {Text} from '../../util/text/Text'
-import {s, gradients, colors} from 'lib/styles'
-import {Trans, msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-
-export function SendReportButton({
-  onPress,
-  isProcessing,
-}: {
-  onPress: () => void
-  isProcessing: boolean
-}) {
-  const {_} = useLingui()
-  // loading state
-  // =
-  if (isProcessing) {
-    return (
-      <View style={[styles.btn, s.mt10]}>
-        <ActivityIndicator />
-      </View>
-    )
-  }
-  return (
-    <TouchableOpacity
-      testID="sendReportBtn"
-      style={s.mt10}
-      onPress={onPress}
-      accessibilityRole="button"
-      accessibilityLabel={_(msg`Report post`)}
-      accessibilityHint={`Reports post with reason and details`}>
-      <LinearGradient
-        colors={[gradients.blueLight.start, gradients.blueLight.end]}
-        start={{x: 0, y: 0}}
-        end={{x: 1, y: 1}}
-        style={[styles.btn]}>
-        <Text style={[s.white, s.bold, s.f18]}>
-          <Trans>Send Report</Trans>
-        </Text>
-      </LinearGradient>
-    </TouchableOpacity>
-  )
-}
-
-const styles = StyleSheet.create({
-  btn: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    justifyContent: 'center',
-    width: '100%',
-    borderRadius: 32,
-    padding: 14,
-    backgroundColor: colors.gray1,
-  },
-})
diff --git a/src/view/com/modals/report/types.ts b/src/view/com/modals/report/types.ts
deleted file mode 100644
index ca947ecbd..000000000
--- a/src/view/com/modals/report/types.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-// TODO: ATM, @atproto/api does not export ids but it does have these listed at @atproto/api/client/lexicons
-// once we start exporting the ids from the @atproto/ap package, replace these hardcoded ones
-export enum CollectionId {
-  FeedGenerator = 'app.bsky.feed.generator',
-  Profile = 'app.bsky.actor.profile',
-  List = 'app.bsky.graph.list',
-  Post = 'app.bsky.feed.post',
-}
diff --git a/src/view/com/modals/util.tsx b/src/view/com/modals/util.tsx
index 06f394ec4..c047a0523 100644
--- a/src/view/com/modals/util.tsx
+++ b/src/view/com/modals/util.tsx
@@ -1,4 +1,4 @@
 export {
   BottomSheetScrollView as ScrollView,
   BottomSheetTextInput as TextInput,
-} from '@gorhom/bottom-sheet'
+} from '@discord/bottom-sheet/src'
diff --git a/src/view/com/notifications/FeedItem.tsx b/src/view/com/notifications/FeedItem.tsx
index f037097df..78b1677c3 100644
--- a/src/view/com/notifications/FeedItem.tsx
+++ b/src/view/com/notifications/FeedItem.tsx
@@ -11,9 +11,10 @@ import {
   AppBskyFeedDefs,
   AppBskyFeedPost,
   ModerationOpts,
-  ProfileModeration,
+  ModerationDecision,
   moderateProfile,
   AppBskyEmbedRecordWithMedia,
+  AppBskyActorDefs,
 } from '@atproto/api'
 import {AtUri} from '@atproto/api'
 import {
@@ -54,7 +55,8 @@ interface Author {
   handle: string
   displayName?: string
   avatar?: string
-  moderation: ProfileModeration
+  moderation: ModerationDecision
+  associated?: AppBskyActorDefs.ProfileAssociated
 }
 
 let FeedItem = ({
@@ -100,6 +102,7 @@ let FeedItem = ({
         displayName: item.notification.author.displayName,
         avatar: item.notification.author.avatar,
         moderation: moderateProfile(item.notification.author, moderationOpts),
+        associated: item.notification.author.associated,
       },
       ...(item.additional?.map(({author}) => {
         return {
@@ -109,6 +112,7 @@ let FeedItem = ({
           displayName: author.displayName,
           avatar: author.avatar,
           moderation: moderateProfile(author, moderationOpts),
+          associated: author.associated,
         }
       }) || []),
     ]
@@ -182,7 +186,6 @@ let FeedItem = ({
       testID={`feedItem-by-${item.notification.author.handle}`}
       style={[
         styles.outer,
-        pal.view,
         pal.border,
         item.notification.isRead
           ? undefined
@@ -228,6 +231,7 @@ let FeedItem = ({
               text={sanitizeDisplayName(
                 authors[0].displayName || authors[0].handle,
               )}
+              disableMismatchWarning
             />
             {authors.length > 1 ? (
               <>
@@ -336,7 +340,8 @@ function CondensedAuthorsList({
           did={authors[0].did}
           handle={authors[0].handle}
           avatar={authors[0].avatar}
-          moderation={authors[0].moderation.avatar}
+          moderation={authors[0].moderation.ui('avatar')}
+          type={authors[0].associated?.labeler ? 'labeler' : 'user'}
         />
       </View>
     )
@@ -354,7 +359,8 @@ function CondensedAuthorsList({
             <UserAvatar
               size={35}
               avatar={author.avatar}
-              moderation={author.moderation.avatar}
+              moderation={author.moderation.ui('avatar')}
+              type={author.associated?.labeler ? 'labeler' : 'user'}
             />
           </View>
         ))}
@@ -412,7 +418,8 @@ function ExpandedAuthorsList({
             <UserAvatar
               size={35}
               avatar={author.avatar}
-              moderation={author.moderation.avatar}
+              moderation={author.moderation.ui('avatar')}
+              type={author.associated?.labeler ? 'labeler' : 'user'}
             />
           </View>
           <View style={s.flex1}>
diff --git a/src/view/com/pager/FeedsTabBar.tsx b/src/view/com/pager/FeedsTabBar.tsx
deleted file mode 100644
index aa0ba7b24..000000000
--- a/src/view/com/pager/FeedsTabBar.tsx
+++ /dev/null
@@ -1 +0,0 @@
-export * from './FeedsTabBarMobile'
diff --git a/src/view/com/pager/FeedsTabBar.web.tsx b/src/view/com/pager/FeedsTabBar.web.tsx
deleted file mode 100644
index 9fe03b7e9..000000000
--- a/src/view/com/pager/FeedsTabBar.web.tsx
+++ /dev/null
@@ -1,155 +0,0 @@
-import React from 'react'
-import {View, StyleSheet} from 'react-native'
-import Animated from 'react-native-reanimated'
-import {TabBar} from 'view/com/pager/TabBar'
-import {RenderTabBarFnProps} from 'view/com/pager/Pager'
-import {usePalette} from 'lib/hooks/usePalette'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {FeedsTabBar as FeedsTabBarMobile} from './FeedsTabBarMobile'
-import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode'
-import {useShellLayout} from '#/state/shell/shell-layout'
-import {usePinnedFeedsInfos} from '#/state/queries/feed'
-import {useSession} from '#/state/session'
-import {TextLink} from '#/view/com/util/Link'
-import {CenteredView} from '../util/Views'
-import {isWeb} from 'platform/detection'
-import {useNavigation} from '@react-navigation/native'
-import {NavigationProp} from 'lib/routes/types'
-
-export function FeedsTabBar(
-  props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void},
-) {
-  const {isMobile, isTablet} = useWebMediaQueries()
-  const {hasSession} = useSession()
-
-  if (isMobile) {
-    return <FeedsTabBarMobile {...props} />
-  } else if (isTablet) {
-    if (hasSession) {
-      return <FeedsTabBarTablet {...props} />
-    } else {
-      return <FeedsTabBarPublic />
-    }
-  } else {
-    return null
-  }
-}
-
-function FeedsTabBarPublic() {
-  const pal = usePalette('default')
-  const {isSandbox} = useSession()
-
-  return (
-    <CenteredView sideBorders>
-      <View
-        style={[
-          pal.view,
-          {
-            flexDirection: 'row',
-            alignItems: 'center',
-            justifyContent: 'space-between',
-            paddingHorizontal: 18,
-            paddingVertical: 12,
-          },
-        ]}>
-        <TextLink
-          type="title-lg"
-          href="/"
-          style={[pal.text, {fontWeight: 'bold'}]}
-          text={
-            <>
-              {isSandbox ? 'SANDBOX' : 'Bluesky'}{' '}
-              {/*hasNew && (
-                <View
-                  style={{
-                    top: -8,
-                    backgroundColor: colors.blue3,
-                    width: 8,
-                    height: 8,
-                    borderRadius: 4,
-                  }}
-                />
-              )*/}
-            </>
-          }
-          // onPress={emitSoftReset}
-        />
-      </View>
-    </CenteredView>
-  )
-}
-
-function FeedsTabBarTablet(
-  props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void},
-) {
-  const {feeds, hasPinnedCustom} = usePinnedFeedsInfos()
-  const pal = usePalette('default')
-  const {hasSession} = useSession()
-  const navigation = useNavigation<NavigationProp>()
-  const {headerMinimalShellTransform} = useMinimalShellMode()
-  const {headerHeight} = useShellLayout()
-
-  const items = React.useMemo(() => {
-    if (!hasSession) return []
-
-    const pinnedNames = feeds.map(f => f.displayName)
-
-    if (!hasPinnedCustom) {
-      return pinnedNames.concat('Feeds ✨')
-    }
-    return pinnedNames
-  }, [hasSession, hasPinnedCustom, feeds])
-
-  const onPressDiscoverFeeds = React.useCallback(() => {
-    if (isWeb) {
-      navigation.navigate('Feeds')
-    } else {
-      navigation.navigate('FeedsTab')
-      navigation.popToTop()
-    }
-  }, [navigation])
-
-  const onSelect = React.useCallback(
-    (index: number) => {
-      if (hasSession && !hasPinnedCustom && index === items.length - 1) {
-        onPressDiscoverFeeds()
-      } else if (props.onSelect) {
-        props.onSelect(index)
-      }
-    },
-    [items.length, onPressDiscoverFeeds, props, hasSession, hasPinnedCustom],
-  )
-
-  return (
-    // @ts-ignore the type signature for transform wrong here, translateX and translateY need to be in separate objects -prf
-    <Animated.View
-      style={[pal.view, pal.border, styles.tabBar, headerMinimalShellTransform]}
-      onLayout={e => {
-        headerHeight.value = e.nativeEvent.layout.height
-      }}>
-      <TabBar
-        key={items.join(',')}
-        {...props}
-        onSelect={onSelect}
-        items={items}
-        indicatorColor={pal.colors.link}
-      />
-    </Animated.View>
-  )
-}
-
-const styles = StyleSheet.create({
-  tabBar: {
-    // @ts-ignore Web only
-    position: 'sticky',
-    zIndex: 1,
-    // @ts-ignore Web only -prf
-    left: 'calc(50% - 300px)',
-    width: 600,
-    top: 0,
-    flexDirection: 'row',
-    alignItems: 'center',
-    borderLeftWidth: 1,
-    borderRightWidth: 1,
-  },
-})
diff --git a/src/view/com/pager/FixedTouchableHighlight.tsx b/src/view/com/pager/FixedTouchableHighlight.tsx
deleted file mode 100644
index d07196975..000000000
--- a/src/view/com/pager/FixedTouchableHighlight.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-// FixedTouchableHighlight.tsx
-import React, {ComponentProps, useRef} from 'react'
-import {GestureResponderEvent, TouchableHighlight} from 'react-native'
-
-type Position = {pageX: number; pageY: number}
-
-export default function FixedTouchableHighlight({
-  onPress,
-  onPressIn,
-  ...props
-}: ComponentProps<typeof TouchableHighlight>) {
-  const _touchActivatePositionRef = useRef<Position | null>(null)
-
-  function _onPressIn(e: GestureResponderEvent) {
-    const {pageX, pageY} = e.nativeEvent
-
-    _touchActivatePositionRef.current = {
-      pageX,
-      pageY,
-    }
-
-    onPressIn?.(e)
-  }
-
-  function _onPress(e: GestureResponderEvent) {
-    const {pageX, pageY} = e.nativeEvent
-
-    const absX = Math.abs(_touchActivatePositionRef.current?.pageX! - pageX)
-    const absY = Math.abs(_touchActivatePositionRef.current?.pageY! - pageY)
-
-    const dragged = absX > 2 || absY > 2
-    if (!dragged) {
-      onPress?.(e)
-    }
-  }
-
-  return (
-    <TouchableHighlight onPressIn={_onPressIn} onPress={_onPress} {...props}>
-      {props.children}
-    </TouchableHighlight>
-  )
-}
diff --git a/src/view/com/pager/PagerWithHeader.tsx b/src/view/com/pager/PagerWithHeader.tsx
index 938c1e7e8..aa110682a 100644
--- a/src/view/com/pager/PagerWithHeader.tsx
+++ b/src/view/com/pager/PagerWithHeader.tsx
@@ -233,36 +233,29 @@ let PagerTabBar = ({
       },
     ],
   }))
-  const pendingHeaderHeight = React.useRef<null | number>(null)
+  const headerRef = React.useRef(null)
   return (
     <Animated.View
       pointerEvents="box-none"
       style={[styles.tabBarMobile, headerTransform]}>
-      <View
-        pointerEvents="box-none"
-        collapsable={false}
-        onLayout={e => {
-          if (isHeaderReady) {
-            onHeaderOnlyLayout(e.nativeEvent.layout.height)
-            pendingHeaderHeight.current = null
-          } else {
-            // Stash it away for when `isHeaderReady` turns `true` later.
-            pendingHeaderHeight.current = e.nativeEvent.layout.height
-          }
-        }}>
+      <View ref={headerRef} pointerEvents="box-none" collapsable={false}>
         {renderHeader?.()}
         {
-          // When `isHeaderReady` turns `true`, we want to send the parent layout.
-          // However, if that didn't lead to a layout change, parent `onLayout` wouldn't get called again.
-          // We're conditionally rendering an empty view so that we can send the last measurement.
+          // It wouldn't be enough to place `onLayout` on the parent node because
+          // this would risk measuring before `isHeaderReady` has turned `true`.
+          // Instead, we'll render a brand node conditionally and get fresh layout.
           isHeaderReady && (
             <View
+              // It wouldn't be enough to do this in a `ref` of an effect because,
+              // even if `isHeaderReady` might have turned `true`, the associated
+              // layout might not have been performed yet on the native side.
               onLayout={() => {
-                // We're assuming the parent `onLayout` already ran (parent -> child ordering).
-                if (pendingHeaderHeight.current !== null) {
-                  onHeaderOnlyLayout(pendingHeaderHeight.current)
-                  pendingHeaderHeight.current = null
-                }
+                // @ts-ignore
+                headerRef.current?.measure(
+                  (_x: number, _y: number, _width: number, height: number) => {
+                    onHeaderOnlyLayout(height)
+                  },
+                )
               }}
             />
           )
diff --git a/src/view/com/pager/TabBar.tsx b/src/view/com/pager/TabBar.tsx
index dadcfcebd..ff8acd60c 100644
--- a/src/view/com/pager/TabBar.tsx
+++ b/src/view/com/pager/TabBar.tsx
@@ -4,8 +4,8 @@ import {Text} from '../util/text/Text'
 import {PressableWithHover} from '../util/PressableWithHover'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {isWeb} from 'platform/detection'
 import {DraggableScrollView} from './DraggableScrollView'
+import {isNative} from '#/platform/detection'
 
 export interface TabBarProps {
   testID?: string
@@ -16,6 +16,10 @@ export interface TabBarProps {
   onPressSelected?: (index: number) => void
 }
 
+// How much of the previous/next item we're showing
+// to give the user a hint there's more to scroll.
+const OFFSCREEN_ITEM_WIDTH = 20
+
 export function TabBar({
   testID,
   selectedPage,
@@ -26,19 +30,68 @@ export function TabBar({
 }: TabBarProps) {
   const pal = usePalette('default')
   const scrollElRef = useRef<ScrollView>(null)
+  const itemRefs = useRef<Array<Element>>([])
   const [itemXs, setItemXs] = useState<number[]>([])
   const indicatorStyle = useMemo(
     () => ({borderBottomColor: indicatorColor || pal.colors.link}),
     [indicatorColor, pal],
   )
   const {isDesktop, isTablet} = useWebMediaQueries()
+  const styles = isDesktop || isTablet ? desktopStyles : mobileStyles
 
-  // scrolls to the selected item when the page changes
   useEffect(() => {
-    scrollElRef.current?.scrollTo({
-      x: itemXs[selectedPage] || 0,
-    })
-  }, [scrollElRef, itemXs, selectedPage])
+    if (isNative) {
+      // On native, the primary interaction is swiping.
+      // We adjust the scroll little by little on every tab change.
+      // Scroll into view but keep the end of the previous item visible.
+      let x = itemXs[selectedPage] || 0
+      x = Math.max(0, x - OFFSCREEN_ITEM_WIDTH)
+      scrollElRef.current?.scrollTo({x})
+    } else {
+      // On the web, the primary interaction is tapping.
+      // Scrolling under tap feels disorienting so only adjust the scroll offset
+      // when tapping on an item out of view--and we adjust by almost an entire page.
+      const parent = scrollElRef?.current?.getScrollableNode?.()
+      if (!parent) {
+        return
+      }
+      const parentRect = parent.getBoundingClientRect()
+      if (!parentRect) {
+        return
+      }
+      const {
+        left: parentLeft,
+        right: parentRight,
+        width: parentWidth,
+      } = parentRect
+      const child = itemRefs.current[selectedPage]
+      if (!child) {
+        return
+      }
+      const childRect = child.getBoundingClientRect?.()
+      if (!childRect) {
+        return
+      }
+      const {left: childLeft, right: childRight, width: childWidth} = childRect
+      let dx = 0
+      if (childRight >= parentRight) {
+        dx += childRight - parentRight
+        dx += parentWidth - childWidth - OFFSCREEN_ITEM_WIDTH
+      } else if (childLeft <= parentLeft) {
+        dx -= parentLeft - childLeft
+        dx -= parentWidth - childWidth - OFFSCREEN_ITEM_WIDTH
+      }
+      let x = parent.scrollLeft + dx
+      x = Math.max(0, x)
+      x = Math.min(x, parent.scrollWidth - parentWidth)
+      if (dx !== 0) {
+        parent.scroll({
+          left: x,
+          behavior: 'smooth',
+        })
+      }
+    }
+  }, [scrollElRef, itemXs, selectedPage, styles])
 
   const onPressItem = useCallback(
     (index: number) => {
@@ -63,8 +116,6 @@ export function TabBar({
     [],
   )
 
-  const styles = isDesktop || isTablet ? desktopStyles : mobileStyles
-
   return (
     <View testID={testID} style={[pal.view, styles.outer]}>
       <DraggableScrollView
@@ -79,20 +130,24 @@ export function TabBar({
             <PressableWithHover
               testID={`${testID}-selector-${i}`}
               key={`${item}-${i}`}
+              ref={node => (itemRefs.current[i] = node)}
               onLayout={e => onItemLayout(e, i)}
-              style={[styles.item, selected && indicatorStyle]}
+              style={styles.item}
               hoverStyle={pal.viewLight}
               onPress={() => onPressItem(i)}>
-              <Text
-                type={isDesktop || isTablet ? 'xl-bold' : 'lg-bold'}
-                testID={testID ? `${testID}-${item}` : undefined}
-                style={selected ? pal.text : pal.textLight}>
-                {item}
-              </Text>
+              <View style={[styles.itemInner, selected && indicatorStyle]}>
+                <Text
+                  type={isDesktop || isTablet ? 'xl-bold' : 'lg-bold'}
+                  testID={testID ? `${testID}-${item}` : undefined}
+                  style={selected ? pal.text : pal.textLight}>
+                  {item}
+                </Text>
+              </View>
             </PressableWithHover>
           )
         })}
       </DraggableScrollView>
+      <View style={[pal.border, styles.outerBottomBorder]} />
     </View>
   )
 }
@@ -103,18 +158,25 @@ const desktopStyles = StyleSheet.create({
     width: 598,
   },
   contentContainer: {
-    columnGap: 8,
-    marginLeft: 14,
-    paddingRight: 14,
+    paddingHorizontal: 0,
     backgroundColor: 'transparent',
   },
   item: {
     paddingTop: 14,
+    paddingHorizontal: 14,
+    justifyContent: 'center',
+  },
+  itemInner: {
     paddingBottom: 12,
-    paddingHorizontal: 10,
     borderBottomWidth: 3,
     borderBottomColor: 'transparent',
-    justifyContent: 'center',
+  },
+  outerBottomBorder: {
+    position: 'absolute',
+    left: 0,
+    right: 0,
+    bottom: -1,
+    borderBottomWidth: 1,
   },
 })
 
@@ -123,17 +185,24 @@ const mobileStyles = StyleSheet.create({
     flexDirection: 'row',
   },
   contentContainer: {
-    columnGap: isWeb ? 0 : 20,
-    marginLeft: isWeb ? 0 : 18,
-    paddingRight: isWeb ? 0 : 36,
     backgroundColor: 'transparent',
+    paddingHorizontal: 8,
   },
   item: {
     paddingTop: 10,
+    paddingHorizontal: 10,
+    justifyContent: 'center',
+  },
+  itemInner: {
     paddingBottom: 10,
-    paddingHorizontal: isWeb ? 8 : 0,
     borderBottomWidth: 3,
     borderBottomColor: 'transparent',
-    justifyContent: 'center',
+  },
+  outerBottomBorder: {
+    position: 'absolute',
+    left: 0,
+    right: 0,
+    bottom: -1,
+    borderBottomWidth: 1,
   },
 })
diff --git a/src/view/com/post-thread/PostLikedBy.tsx b/src/view/com/post-thread/PostLikedBy.tsx
index 55463dc13..0760ed7ff 100644
--- a/src/view/com/post-thread/PostLikedBy.tsx
+++ b/src/view/com/post-thread/PostLikedBy.tsx
@@ -8,7 +8,7 @@ import {ProfileCardWithFollowBtn} from '../profile/ProfileCard'
 import {logger} from '#/logger'
 import {LoadingScreen} from '../util/LoadingScreen'
 import {useResolveUriQuery} from '#/state/queries/resolve-uri'
-import {usePostLikedByQuery} from '#/state/queries/post-liked-by'
+import {useLikedByQuery} from '#/state/queries/post-liked-by'
 import {cleanError} from '#/lib/strings/errors'
 
 export function PostLikedBy({uri}: {uri: string}) {
@@ -28,7 +28,7 @@ export function PostLikedBy({uri}: {uri: string}) {
     isError,
     error,
     refetch,
-  } = usePostLikedByQuery(resolvedUri?.uri)
+  } = useLikedByQuery(resolvedUri?.uri)
   const likes = useMemo(() => {
     if (data?.pages) {
       return data.pages.flatMap(page => page.likes)
diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx
index ef3d7e2b6..c1159379d 100644
--- a/src/view/com/post-thread/PostThread.tsx
+++ b/src/view/com/post-thread/PostThread.tsx
@@ -1,66 +1,68 @@
 import React, {useEffect, useRef} from 'react'
-import {
-  ActivityIndicator,
-  Pressable,
-  StyleSheet,
-  TouchableOpacity,
-  View,
-} from 'react-native'
+import {StyleSheet, useWindowDimensions, View} from 'react-native'
 import {AppBskyFeedDefs} from '@atproto/api'
-import {CenteredView} from '../util/Views'
-import {LoadingScreen} from '../util/LoadingScreen'
-import {List, ListMethods} from '../util/List'
-import {
-  FontAwesomeIcon,
-  FontAwesomeIconStyle,
-} from '@fortawesome/react-native-fontawesome'
-import {PostThreadItem} from './PostThreadItem'
-import {ComposePrompt} from '../composer/Prompt'
-import {ViewHeader} from '../util/ViewHeader'
-import {ErrorMessage} from '../util/error/ErrorMessage'
-import {Text} from '../util/text/Text'
-import {s} from 'lib/styles'
-import {usePalette} from 'lib/hooks/usePalette'
-import {useSetTitle} from 'lib/hooks/useSetTitle'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
+import {isAndroid, isNative, isWeb} from '#/platform/detection'
 import {
+  sortThread,
+  ThreadBlocked,
   ThreadNode,
+  ThreadNotFound,
   ThreadPost,
   usePostThreadQuery,
-  sortThread,
 } from '#/state/queries/post-thread'
-import {useNavigation} from '@react-navigation/native'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {NavigationProp} from 'lib/routes/types'
-import {sanitizeDisplayName} from 'lib/strings/display-names'
-import {cleanError} from '#/lib/strings/errors'
-import {Trans, msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
 import {
-  UsePreferencesQueryResponse,
   useModerationOpts,
   usePreferencesQuery,
 } from '#/state/queries/preferences'
 import {useSession} from '#/state/session'
-import {isAndroid, isNative} from '#/platform/detection'
-import {logger} from '#/logger'
-import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
+import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
+import {usePalette} from 'lib/hooks/usePalette'
+import {useSetTitle} from 'lib/hooks/useSetTitle'
+import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
+import {sanitizeDisplayName} from 'lib/strings/display-names'
+import {cleanError} from 'lib/strings/errors'
+import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
+import {ComposePrompt} from '../composer/Prompt'
+import {List, ListMethods} from '../util/List'
+import {Text} from '../util/text/Text'
+import {ViewHeader} from '../util/ViewHeader'
+import {PostThreadItem} from './PostThreadItem'
+
+// FlatList maintainVisibleContentPosition breaks if too many items
+// are prepended. This seems to be an optimal number based on *shrug*.
+const PARENTS_CHUNK_SIZE = 15
 
-const MAINTAIN_VISIBLE_CONTENT_POSITION = {minIndexForVisible: 1}
+const MAINTAIN_VISIBLE_CONTENT_POSITION = {
+  // We don't insert any elements before the root row while loading.
+  // So the row we want to use as the scroll anchor is the first row.
+  minIndexForVisible: 0,
+}
 
 const TOP_COMPONENT = {_reactKey: '__top_component__'}
 const REPLY_PROMPT = {_reactKey: '__reply__'}
-const DELETED = {_reactKey: '__deleted__'}
-const BLOCKED = {_reactKey: '__blocked__'}
-const CHILD_SPINNER = {_reactKey: '__child_spinner__'}
 const LOAD_MORE = {_reactKey: '__load_more__'}
-const BOTTOM_COMPONENT = {_reactKey: '__bottom_component__'}
 
-type YieldedItem =
-  | ThreadPost
+type YieldedItem = ThreadPost | ThreadBlocked | ThreadNotFound
+type RowItem =
+  | YieldedItem
+  // TODO: TS doesn't actually enforce it's one of these, it only enforces matching shape.
   | typeof TOP_COMPONENT
   | typeof REPLY_PROMPT
-  | typeof DELETED
-  | typeof BLOCKED
+  | typeof LOAD_MORE
+
+type ThreadSkeletonParts = {
+  parents: YieldedItem[]
+  highlightedPost: ThreadNode
+  replies: YieldedItem[]
+}
+
+const keyExtractor = (item: RowItem) => {
+  return item._reactKey
+}
 
 export function PostThread({
   uri,
@@ -69,17 +71,30 @@ export function PostThread({
 }: {
   uri: string | undefined
   onCanReply: (canReply: boolean) => void
-  onPressReply: () => void
+  onPressReply: () => unknown
 }) {
+  const {hasSession} = useSession()
+  const {_} = useLingui()
+  const pal = usePalette('default')
+  const {isMobile, isTabletOrMobile} = useWebMediaQueries()
+  const initialNumToRender = useInitialNumToRender()
+  const {height: windowHeight} = useWindowDimensions()
+
+  const {data: preferences} = usePreferencesQuery()
   const {
-    isLoading,
-    isError,
-    error,
+    isFetching,
+    isError: isThreadError,
+    error: threadError,
     refetch,
     data: thread,
   } = usePostThreadQuery(uri)
-  const {data: preferences} = usePreferencesQuery()
 
+  const treeView = React.useMemo(
+    () =>
+      !!preferences?.threadViewPrefs?.lab_treeViewEnabled &&
+      hasBranchingReplies(thread),
+    [preferences?.threadViewPrefs, thread],
+  )
   const rootPost = thread?.type === 'post' ? thread.post : undefined
   const rootPostRecord = thread?.type === 'post' ? thread.record : undefined
 
@@ -89,14 +104,23 @@ export function PostThread({
       rootPost && moderationOpts
         ? moderatePost(rootPost, moderationOpts)
         : undefined
-
-    const cause = mod?.content.cause
-
-    return cause
-      ? cause.type === 'label' && cause.labelDef.id === '!no-unauthenticated'
-      : false
+    return !!mod
+      ?.ui('contentList')
+      .blurs.find(
+        cause =>
+          cause.type === 'label' &&
+          cause.labelDef.identifier === '!no-unauthenticated',
+      )
   }, [rootPost, moderationOpts])
 
+  // Values used for proper rendering of parents
+  const ref = useRef<ListMethods>(null)
+  const highlightedPostRef = useRef<View | null>(null)
+  const [maxParents, setMaxParents] = React.useState(
+    isWeb ? Infinity : PARENTS_CHUNK_SIZE,
+  )
+  const [maxReplies, setMaxReplies] = React.useState(50)
+
   useSetTitle(
     rootPost && !isNoPwi
       ? `${sanitizeDisplayName(
@@ -104,147 +128,164 @@ export function PostThread({
         )}: "${rootPostRecord!.text}"`
       : '',
   )
+
+  // On native, this is going to start out `true`. We'll toggle it to `false` after the initial render if flushed.
+  // This ensures that the first render contains no parents--even if they are already available in the cache.
+  // We need to delay showing them so that we can use maintainVisibleContentPosition to keep the main post on screen.
+  // On the web this is not necessary because we can synchronously adjust the scroll in onContentSizeChange instead.
+  const [deferParents, setDeferParents] = React.useState(isNative)
+
+  const skeleton = React.useMemo(() => {
+    const threadViewPrefs = preferences?.threadViewPrefs
+    if (!threadViewPrefs || !thread) return null
+
+    return createThreadSkeleton(
+      sortThread(thread, threadViewPrefs),
+      hasSession,
+      treeView,
+    )
+  }, [thread, preferences?.threadViewPrefs, hasSession, treeView])
+
+  const error = React.useMemo(() => {
+    if (AppBskyFeedDefs.isNotFoundPost(thread)) {
+      return {
+        title: _(msg`Post not found`),
+        message: _(msg`The post may have been deleted.`),
+      }
+    } else if (skeleton?.highlightedPost.type === 'blocked') {
+      return {
+        title: _(msg`Post hidden`),
+        message: _(
+          msg`You have blocked the author or you have been blocked by the author.`,
+        ),
+      }
+    } else if (threadError?.message.startsWith('Post not found')) {
+      return {
+        title: _(msg`Post not found`),
+        message: _(msg`The post may have been deleted.`),
+      }
+    } else if (isThreadError) {
+      return {
+        message: threadError ? cleanError(threadError) : undefined,
+      }
+    }
+
+    return null
+  }, [thread, skeleton?.highlightedPost, isThreadError, _, threadError])
+
   useEffect(() => {
-    if (rootPost) {
+    if (error) {
+      onCanReply(false)
+    } else if (rootPost) {
       onCanReply(!rootPost.viewer?.replyDisabled)
     }
-  }, [rootPost, onCanReply])
-
-  if (isError || AppBskyFeedDefs.isNotFoundPost(thread)) {
-    return (
-      <PostThreadError
-        error={error}
-        notFound={AppBskyFeedDefs.isNotFoundPost(thread)}
-        onRefresh={refetch}
-      />
-    )
-  }
-  if (AppBskyFeedDefs.isBlockedPost(thread)) {
-    return <PostThreadBlocked />
-  }
-  if (!thread || isLoading || !preferences) {
-    return <LoadingScreen />
-  }
-  return (
-    <PostThreadLoaded
-      thread={thread}
-      threadViewPrefs={preferences.threadViewPrefs}
-      onRefresh={refetch}
-      onPressReply={onPressReply}
-    />
-  )
-}
-
-function PostThreadLoaded({
-  thread,
-  threadViewPrefs,
-  onRefresh,
-  onPressReply,
-}: {
-  thread: ThreadNode
-  threadViewPrefs: UsePreferencesQueryResponse['threadViewPrefs']
-  onRefresh: () => void
-  onPressReply: () => void
-}) {
-  const {hasSession} = useSession()
-  const {_} = useLingui()
-  const pal = usePalette('default')
-  const {isMobile, isTabletOrMobile} = useWebMediaQueries()
-  const ref = useRef<ListMethods>(null)
-  const highlightedPostRef = useRef<View | null>(null)
-  const needsScrollAdjustment = useRef<boolean>(
-    !isNative || // web always uses scroll adjustment
-      (thread.type === 'post' && !thread.ctx.isParentLoading), // native only does it when not loading from placeholder
-  )
-  const [maxVisible, setMaxVisible] = React.useState(100)
-  const [isPTRing, setIsPTRing] = React.useState(false)
-  const treeView = React.useMemo(
-    () => !!threadViewPrefs.lab_treeViewEnabled && hasBranchingReplies(thread),
-    [threadViewPrefs, thread],
-  )
+  }, [rootPost, onCanReply, error])
 
   // construct content
   const posts = React.useMemo(() => {
-    let arr = Array.from(
-      flattenThreadSkeleton(
-        sortThread(thread, threadViewPrefs),
-        hasSession,
-        treeView,
-      ),
-    )
-    if (arr.length > maxVisible) {
-      arr = arr.slice(0, maxVisible).concat([LOAD_MORE])
-    }
-    if (arr.indexOf(CHILD_SPINNER) === -1) {
-      arr.push(BOTTOM_COMPONENT)
+    if (!skeleton) return []
+
+    const {parents, highlightedPost, replies} = skeleton
+    let arr: RowItem[] = []
+    if (highlightedPost.type === 'post') {
+      const isRoot =
+        !highlightedPost.parent && !highlightedPost.ctx.isParentLoading
+      if (isRoot) {
+        // No parents to load.
+        arr.push(TOP_COMPONENT)
+      } else {
+        if (highlightedPost.ctx.isParentLoading || deferParents) {
+          // We're loading parents of the highlighted post.
+          // In this case, we don't render anything above the post.
+          // If you add something here, you'll need to update both
+          // maintainVisibleContentPosition and onContentSizeChange
+          // to "hold onto" the correct row instead of the first one.
+        } else {
+          // Everything is loaded
+          let startIndex = Math.max(0, parents.length - maxParents)
+          if (startIndex === 0) {
+            arr.push(TOP_COMPONENT)
+          } else {
+            // When progressively revealing parents, rendering a placeholder
+            // here will cause scrolling jumps. Don't add it unless you test it.
+            // QT'ing this thread is a great way to test all the scrolling hacks:
+            // https://bsky.app/profile/www.mozzius.dev/post/3kjqhblh6qk2o
+          }
+          for (let i = startIndex; i < parents.length; i++) {
+            arr.push(parents[i])
+          }
+        }
+      }
+      arr.push(highlightedPost)
+      if (!highlightedPost.post.viewer?.replyDisabled) {
+        arr.push(REPLY_PROMPT)
+      }
+      for (let i = 0; i < replies.length; i++) {
+        arr.push(replies[i])
+        if (i === maxReplies) {
+          break
+        }
+      }
     }
     return arr
-  }, [thread, treeView, maxVisible, threadViewPrefs, hasSession])
-
-  /**
-   * NOTE
-   * Scroll positioning
-   *
-   * This callback is run if needsScrollAdjustment.current == true, which is...
-   *  - On web: always
-   *  - On native: when the placeholder cache is not being used
-   *
-   * It then only runs when viewing a reply, and the goal is to scroll the
-   * reply into view.
-   *
-   * On native, if the placeholder cache is being used then maintainVisibleContentPosition
-   * is a more effective solution, so we use that. Otherwise, typically we're loading from
-   * the react-query cache, so we just need to immediately scroll down to the post.
-   *
-   * On desktop, maintainVisibleContentPosition isn't supported so we just always use
-   * this technique.
-   *
-   * -prf
-   */
-  const onContentSizeChange = React.useCallback(() => {
+  }, [skeleton, deferParents, maxParents, maxReplies])
+
+  // This is only used on the web to keep the post in view when its parents load.
+  // On native, we rely on `maintainVisibleContentPosition` instead.
+  const didAdjustScrollWeb = useRef<boolean>(false)
+  const onContentSizeChangeWeb = React.useCallback(() => {
     // only run once
-    if (!needsScrollAdjustment.current) {
+    if (didAdjustScrollWeb.current) {
       return
     }
-
     // wait for loading to finish
-    if (thread.type === 'post' && !!thread.parent) {
+    if (thread?.type === 'post' && !!thread.parent) {
       function onMeasure(pageY: number) {
         ref.current?.scrollToOffset({
           animated: false,
           offset: pageY,
         })
       }
-      if (isNative) {
-        highlightedPostRef.current?.measure(
-          (_x, _y, _width, _height, _pageX, pageY) => {
-            onMeasure(pageY)
-          },
-        )
-      } else {
-        // Measure synchronously to avoid a layout jump.
-        const domNode = highlightedPostRef.current
-        if (domNode) {
-          const pageY = (domNode as any as Element).getBoundingClientRect().top
-          onMeasure(pageY)
-        }
+      // Measure synchronously to avoid a layout jump.
+      const domNode = highlightedPostRef.current
+      if (domNode) {
+        const pageY = (domNode as any as Element).getBoundingClientRect().top
+        onMeasure(pageY)
       }
-      needsScrollAdjustment.current = false
+      didAdjustScrollWeb.current = true
     }
   }, [thread])
 
-  const onPTR = React.useCallback(async () => {
-    setIsPTRing(true)
-    try {
-      await onRefresh()
-    } catch (err) {
-      logger.error('Failed to refresh posts thread', {message: err})
+  // On native, we reveal parents in chunks. Although they're all already
+  // loaded and FlatList already has its own virtualization, unfortunately FlatList
+  // has a bug that causes the content to jump around if too many items are getting
+  // prepended at once. It also jumps around if items get prepended during scroll.
+  // To work around this, we prepend rows after scroll bumps against the top and rests.
+  const needsBumpMaxParents = React.useRef(false)
+  const onStartReached = React.useCallback(() => {
+    if (skeleton?.parents && maxParents < skeleton.parents.length) {
+      needsBumpMaxParents.current = true
     }
-    setIsPTRing(false)
-  }, [setIsPTRing, onRefresh])
+  }, [maxParents, skeleton?.parents])
+  const bumpMaxParentsIfNeeded = React.useCallback(() => {
+    if (!isNative) {
+      return
+    }
+    if (needsBumpMaxParents.current) {
+      needsBumpMaxParents.current = false
+      setMaxParents(n => n + PARENTS_CHUNK_SIZE)
+    }
+  }, [])
+  const onMomentumScrollEnd = bumpMaxParentsIfNeeded
+  const onScrollToTop = bumpMaxParentsIfNeeded
+
+  const onEndReached = React.useCallback(() => {
+    if (isFetching || posts.length < maxReplies) return
+    setMaxReplies(prev => prev + 50)
+  }, [isFetching, maxReplies, posts.length])
 
   const renderItem = React.useCallback(
-    ({item, index}: {item: YieldedItem; index: number}) => {
+    ({item, index}: {item: RowItem; index: number}) => {
       if (item === TOP_COMPONENT) {
         return isTabletOrMobile ? (
           <ViewHeader
@@ -257,7 +298,7 @@ function PostThreadLoaded({
             {!isMobile && <ComposePrompt onPressCompose={onPressReply} />}
           </View>
         )
-      } else if (item === DELETED) {
+      } else if (isThreadNotFound(item)) {
         return (
           <View style={[pal.border, pal.viewLight, styles.itemContainer]}>
             <Text type="lg-bold" style={pal.textLight}>
@@ -265,7 +306,7 @@ function PostThreadLoaded({
             </Text>
           </View>
         )
-      } else if (item === BLOCKED) {
+      } else if (isThreadBlocked(item)) {
         return (
           <View style={[pal.border, pal.viewLight, styles.itemContainer]}>
             <Text type="lg-bold" style={pal.textLight}>
@@ -273,46 +314,6 @@ function PostThreadLoaded({
             </Text>
           </View>
         )
-      } else if (item === LOAD_MORE) {
-        return (
-          <Pressable
-            onPress={() => setMaxVisible(n => n + 50)}
-            style={[pal.border, pal.view, styles.itemContainer]}
-            accessibilityLabel={_(msg`Load more posts`)}
-            accessibilityHint="">
-            <View
-              style={[
-                pal.viewLight,
-                {paddingHorizontal: 18, paddingVertical: 14, borderRadius: 6},
-              ]}>
-              <Text type="lg-medium" style={pal.text}>
-                <Trans>Load more posts</Trans>
-              </Text>
-            </View>
-          </Pressable>
-        )
-      } else if (item === BOTTOM_COMPONENT) {
-        // HACK
-        // due to some complexities with how flatlist works, this is the easiest way
-        // I could find to get a border positioned directly under the last item
-        // -prf
-        return (
-          <View
-            // @ts-ignore web-only
-            style={{
-              // Leave enough space below that the scroll doesn't jump
-              height: isNative ? 600 : '100vh',
-              borderTopWidth: 1,
-              borderColor: pal.colors.border,
-            }}
-          />
-        )
-      } else if (item === CHILD_SPINNER) {
-        return (
-          <View style={[pal.border, styles.childSpinner]}>
-            <ActivityIndicator />
-          </View>
-        )
       } else if (isThreadPost(item)) {
         const prev = isThreadPost(posts[index - 1])
           ? (posts[index - 1] as ThreadPost)
@@ -320,9 +321,14 @@ function PostThreadLoaded({
         const next = isThreadPost(posts[index - 1])
           ? (posts[index - 1] as ThreadPost)
           : undefined
+        const hasUnrevealedParents =
+          index === 0 &&
+          skeleton?.parents &&
+          maxParents < skeleton.parents.length
         return (
           <View
-            ref={item.ctx.isHighlightedPost ? highlightedPostRef : undefined}>
+            ref={item.ctx.isHighlightedPost ? highlightedPostRef : undefined}
+            onLayout={deferParents ? () => setDeferParents(false) : undefined}>
             <PostThreadItem
               post={item.post}
               record={item.record}
@@ -334,8 +340,10 @@ function PostThreadLoaded({
               hasMore={item.ctx.hasMore}
               showChildReplyLine={item.ctx.showChildReplyLine}
               showParentReplyLine={item.ctx.showParentReplyLine}
-              hasPrecedingItem={!!prev?.ctx.showChildReplyLine}
-              onPostReply={onRefresh}
+              hasPrecedingItem={
+                !!prev?.ctx.showChildReplyLine || !!hasUnrevealedParents
+              }
+              onPostReply={refetch}
             />
           </View>
         )
@@ -345,182 +353,133 @@ function PostThreadLoaded({
     [
       hasSession,
       isTabletOrMobile,
+      _,
       isMobile,
       onPressReply,
       pal.border,
       pal.viewLight,
       pal.textLight,
-      pal.view,
-      pal.text,
-      pal.colors.border,
       posts,
-      onRefresh,
+      skeleton?.parents,
+      maxParents,
+      deferParents,
       treeView,
-      _,
+      refetch,
     ],
   )
 
   return (
-    <List
-      ref={ref}
-      data={posts}
-      initialNumToRender={!isNative ? posts.length : undefined}
-      maintainVisibleContentPosition={
-        !needsScrollAdjustment.current
-          ? MAINTAIN_VISIBLE_CONTENT_POSITION
-          : undefined
-      }
-      keyExtractor={item => item._reactKey}
-      renderItem={renderItem}
-      refreshing={isPTRing}
-      onRefresh={onPTR}
-      onContentSizeChange={onContentSizeChange}
-      style={s.hContentRegion}
-      // @ts-ignore our .web version only -prf
-      desktopFixedHeight
-      removeClippedSubviews={isAndroid ? false : undefined}
-    />
+    <>
+      <ListMaybePlaceholder
+        isLoading={(!preferences || !thread) && !error}
+        isError={!!error}
+        onRetry={refetch}
+        errorTitle={error?.title}
+        errorMessage={error?.message}
+      />
+      {!error && thread && (
+        <List
+          ref={ref}
+          data={posts}
+          renderItem={renderItem}
+          keyExtractor={keyExtractor}
+          onContentSizeChange={isNative ? undefined : onContentSizeChangeWeb}
+          onStartReached={onStartReached}
+          onEndReached={onEndReached}
+          onEndReachedThreshold={2}
+          onMomentumScrollEnd={onMomentumScrollEnd}
+          onScrollToTop={onScrollToTop}
+          maintainVisibleContentPosition={
+            isNative ? MAINTAIN_VISIBLE_CONTENT_POSITION : undefined
+          }
+          // @ts-ignore our .web version only -prf
+          desktopFixedHeight
+          removeClippedSubviews={isAndroid ? false : undefined}
+          ListFooterComponent={
+            <ListFooter
+              isFetching={isFetching}
+              onRetry={refetch}
+              // 300 is based on the minimum height of a post. This is enough extra height for the `maintainVisPos` to
+              // work without causing weird jumps on web or glitches on native
+              height={windowHeight - 200}
+            />
+          }
+          initialNumToRender={initialNumToRender}
+          windowSize={11}
+        />
+      )}
+    </>
   )
 }
 
-function PostThreadBlocked() {
-  const {_} = useLingui()
-  const pal = usePalette('default')
-  const navigation = useNavigation<NavigationProp>()
+function isThreadPost(v: unknown): v is ThreadPost {
+  return !!v && typeof v === 'object' && 'type' in v && v.type === 'post'
+}
 
-  const onPressBack = React.useCallback(() => {
-    if (navigation.canGoBack()) {
-      navigation.goBack()
-    } else {
-      navigation.navigate('Home')
-    }
-  }, [navigation])
+function isThreadNotFound(v: unknown): v is ThreadNotFound {
+  return !!v && typeof v === 'object' && 'type' in v && v.type === 'not-found'
+}
 
-  return (
-    <CenteredView>
-      <View style={[pal.view, pal.border, styles.notFoundContainer]}>
-        <Text type="title-lg" style={[pal.text, s.mb5]}>
-          <Trans>Post hidden</Trans>
-        </Text>
-        <Text type="md" style={[pal.text, s.mb10]}>
-          <Trans>
-            You have blocked the author or you have been blocked by the author.
-          </Trans>
-        </Text>
-        <TouchableOpacity
-          onPress={onPressBack}
-          accessibilityRole="button"
-          accessibilityLabel={_(msg`Back`)}
-          accessibilityHint="">
-          <Text type="2xl" style={pal.link}>
-            <FontAwesomeIcon
-              icon="angle-left"
-              style={[pal.link as FontAwesomeIconStyle, s.mr5]}
-              size={14}
-            />
-            <Trans context="action">Back</Trans>
-          </Text>
-        </TouchableOpacity>
-      </View>
-    </CenteredView>
-  )
+function isThreadBlocked(v: unknown): v is ThreadBlocked {
+  return !!v && typeof v === 'object' && 'type' in v && v.type === 'blocked'
 }
 
-function PostThreadError({
-  onRefresh,
-  notFound,
-  error,
-}: {
-  onRefresh: () => void
-  notFound: boolean
-  error: Error | null
-}) {
-  const {_} = useLingui()
-  const pal = usePalette('default')
-  const navigation = useNavigation<NavigationProp>()
+function createThreadSkeleton(
+  node: ThreadNode,
+  hasSession: boolean,
+  treeView: boolean,
+): ThreadSkeletonParts | null {
+  if (!node) return null
 
-  const onPressBack = React.useCallback(() => {
-    if (navigation.canGoBack()) {
-      navigation.goBack()
-    } else {
-      navigation.navigate('Home')
-    }
-  }, [navigation])
-
-  if (notFound) {
-    return (
-      <CenteredView>
-        <View style={[pal.view, pal.border, styles.notFoundContainer]}>
-          <Text type="title-lg" style={[pal.text, s.mb5]}>
-            <Trans>Post not found</Trans>
-          </Text>
-          <Text type="md" style={[pal.text, s.mb10]}>
-            <Trans>The post may have been deleted.</Trans>
-          </Text>
-          <TouchableOpacity
-            onPress={onPressBack}
-            accessibilityRole="button"
-            accessibilityLabel={_(msg`Back`)}
-            accessibilityHint="">
-            <Text type="2xl" style={pal.link}>
-              <FontAwesomeIcon
-                icon="angle-left"
-                style={[pal.link as FontAwesomeIconStyle, s.mr5]}
-                size={14}
-              />
-              <Trans>Back</Trans>
-            </Text>
-          </TouchableOpacity>
-        </View>
-      </CenteredView>
-    )
+  return {
+    parents: Array.from(flattenThreadParents(node, hasSession)),
+    highlightedPost: node,
+    replies: Array.from(flattenThreadReplies(node, hasSession, treeView)),
   }
-  return (
-    <CenteredView>
-      <ErrorMessage message={cleanError(error)} onPressTryAgain={onRefresh} />
-    </CenteredView>
-  )
 }
 
-function isThreadPost(v: unknown): v is ThreadPost {
-  return !!v && typeof v === 'object' && 'type' in v && v.type === 'post'
+function* flattenThreadParents(
+  node: ThreadNode,
+  hasSession: boolean,
+): Generator<YieldedItem, void> {
+  if (node.type === 'post') {
+    if (node.parent) {
+      yield* flattenThreadParents(node.parent, hasSession)
+    }
+    if (!node.ctx.isHighlightedPost) {
+      yield node
+    }
+  } else if (node.type === 'not-found') {
+    yield node
+  } else if (node.type === 'blocked') {
+    yield node
+  }
 }
 
-function* flattenThreadSkeleton(
+function* flattenThreadReplies(
   node: ThreadNode,
   hasSession: boolean,
   treeView: boolean,
-  isTraversingReplies: boolean = false,
 ): Generator<YieldedItem, void> {
   if (node.type === 'post') {
-    if (!node.ctx.isParentLoading) {
-      if (node.parent) {
-        yield* flattenThreadSkeleton(node.parent, hasSession, treeView, false)
-      } else if (!isTraversingReplies) {
-        yield TOP_COMPONENT
-      }
-    }
-    if (!hasSession && node.ctx.depth > 0 && hasPwiOptOut(node)) {
+    if (!hasSession && hasPwiOptOut(node)) {
       return
     }
-    yield node
-    if (node.ctx.isHighlightedPost && !node.post.viewer?.replyDisabled) {
-      yield REPLY_PROMPT
+    if (!node.ctx.isHighlightedPost) {
+      yield node
     }
     if (node.replies?.length) {
       for (const reply of node.replies) {
-        yield* flattenThreadSkeleton(reply, hasSession, treeView, true)
+        yield* flattenThreadReplies(reply, hasSession, treeView)
         if (!treeView && !node.ctx.isHighlightedPost) {
           break
         }
       }
-    } else if (node.ctx.isChildLoading) {
-      yield CHILD_SPINNER
     }
   } else if (node.type === 'not-found') {
-    yield DELETED
+    yield node
   } else if (node.type === 'blocked') {
-    yield BLOCKED
+    yield node
   }
 }
 
@@ -528,7 +487,10 @@ function hasPwiOptOut(node: ThreadPost) {
   return !!node.post.author.labels?.find(l => l.val === '!no-unauthenticated')
 }
 
-function hasBranchingReplies(node: ThreadNode) {
+function hasBranchingReplies(node?: ThreadNode) {
+  if (!node) {
+    return false
+  }
   if (node.type !== 'post') {
     return false
   }
@@ -542,20 +504,9 @@ function hasBranchingReplies(node: ThreadNode) {
 }
 
 const styles = StyleSheet.create({
-  notFoundContainer: {
-    margin: 10,
-    paddingHorizontal: 18,
-    paddingVertical: 14,
-    borderRadius: 6,
-  },
   itemContainer: {
     borderTopWidth: 1,
     paddingHorizontal: 18,
     paddingVertical: 18,
   },
-  childSpinner: {
-    borderTopWidth: 1,
-    paddingTop: 40,
-    paddingBottom: 200,
-  },
 })
diff --git a/src/view/com/post-thread/PostThreadFollowBtn.tsx b/src/view/com/post-thread/PostThreadFollowBtn.tsx
index e5b747cc9..45c3771f5 100644
--- a/src/view/com/post-thread/PostThreadFollowBtn.tsx
+++ b/src/view/com/post-thread/PostThreadFollowBtn.tsx
@@ -42,7 +42,10 @@ function PostThreadFollowBtnLoaded({
   const {isTabletOrDesktop} = useWebMediaQueries()
   const profile: Shadow<AppBskyActorDefs.ProfileViewBasic> =
     useProfileShadow(profileUnshadowed)
-  const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(profile)
+  const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(
+    profile,
+    'PostThreadItem',
+  )
   const requireAuth = useRequireAuth()
 
   const isFollowing = !!profile.viewer?.following
diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx
index 826d0d161..6555bdf73 100644
--- a/src/view/com/post-thread/PostThreadItem.tsx
+++ b/src/view/com/post-thread/PostThreadItem.tsx
@@ -5,13 +5,13 @@ import {
   AppBskyFeedDefs,
   AppBskyFeedPost,
   RichText as RichTextAPI,
-  PostModeration,
+  ModerationDecision,
 } from '@atproto/api'
 import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {PostThreadFollowBtn} from 'view/com/post-thread/PostThreadFollowBtn'
 import {Link, TextLink} from '../util/Link'
-import {RichText} from '../util/text/RichText'
+import {RichText} from '#/components/RichText'
 import {Text} from '../util/text/Text'
 import {PreviewableUserAvatar} from '../util/UserAvatar'
 import {s} from 'lib/styles'
@@ -19,15 +19,14 @@ import {niceDate} from 'lib/strings/time'
 import {sanitizeDisplayName} from 'lib/strings/display-names'
 import {sanitizeHandle} from 'lib/strings/handles'
 import {countLines, pluralize} from 'lib/strings/helpers'
-import {isEmbedByEmbedder} from 'lib/embeds'
 import {getTranslatorLink, isPostInLanguage} from '../../../locale/helpers'
 import {PostMeta} from '../util/PostMeta'
 import {PostEmbeds} from '../util/post-embeds'
 import {PostCtrls} from '../util/post-ctrls/PostCtrls'
-import {PostHider} from '../util/moderation/PostHider'
-import {ContentHider} from '../util/moderation/ContentHider'
-import {PostAlerts} from '../util/moderation/PostAlerts'
-import {PostSandboxWarning} from '../util/PostSandboxWarning'
+import {PostHider} from '../../../components/moderation/PostHider'
+import {ContentHider} from '../../../components/moderation/ContentHider'
+import {PostAlerts} from '../../../components/moderation/PostAlerts'
+import {LabelsOnMyPost} from '../../../components/moderation/LabelsOnMe'
 import {ErrorMessage} from '../util/error/ErrorMessage'
 import {usePalette} from 'lib/hooks/usePalette'
 import {formatCount} from '../util/numeric/format'
@@ -44,6 +43,8 @@ import {Shadow, usePostShadow, POST_TOMBSTONE} from '#/state/cache/post-shadow'
 import {ThreadPost} from '#/state/queries/post-thread'
 import {useSession} from 'state/session'
 import {WhoCanReply} from '../threadgate/WhoCanReply'
+import {LoadingPlaceholder} from '../util/LoadingPlaceholder'
+import {atoms as a} from '#/alf'
 
 export function PostThreadItem({
   post,
@@ -93,6 +94,8 @@ export function PostThreadItem({
   if (richText && moderation) {
     return (
       <PostThreadItemLoaded
+        // Safeguard from clobbering per-post state below:
+        key={postShadowed.uri}
         post={postShadowed}
         prevPost={prevPost}
         nextPost={nextPost}
@@ -144,7 +147,7 @@ let PostThreadItemLoaded = ({
   post: Shadow<AppBskyFeedDefs.PostView>
   record: AppBskyFeedPost.Record
   richText: RichTextAPI
-  moderation: PostModeration
+  moderation: ModerationDecision
   treeView: boolean
   depth: number
   prevPost: ThreadPost | undefined
@@ -164,8 +167,6 @@ let PostThreadItemLoaded = ({
     () => countLines(richText?.text) >= MAX_POST_LINES,
   )
   const {currentAccount} = useSession()
-  const hasEngagement = post.likeCount || post.repostCount
-
   const rootUri = record.reply?.root?.uri || post.uri
   const postHref = React.useMemo(() => {
     const urip = new AtUri(post.uri)
@@ -174,7 +175,6 @@ let PostThreadItemLoaded = ({
   const itemTitle = _(msg`Post by ${post.author.handle}`)
   const authorHref = makeProfileLink(post.author)
   const authorTitle = post.author.handle
-  const isAuthorMuted = post.author.viewer?.muted
   const likesHref = React.useMemo(() => {
     const urip = new AtUri(post.uri)
     return makeProfileLink(post.author, 'post', urip.rkey, 'liked-by')
@@ -205,11 +205,7 @@ let PostThreadItemLoaded = ({
         uri: post.uri,
         cid: post.cid,
         text: record.text,
-        author: {
-          handle: post.author.handle,
-          displayName: post.author.displayName,
-          avatar: post.author.avatar,
-        },
+        author: post.author,
         embed: post.embed,
         moderation,
       },
@@ -248,7 +244,6 @@ let PostThreadItemLoaded = ({
           testID={`postThreadItem-by-${post.author.handle}`}
           style={[styles.outer, styles.outerHighlighted, pal.border, pal.view]}
           accessible={false}>
-          <PostSandboxWarning />
           <View style={[styles.layout]}>
             <View style={[styles.layoutAvi, {paddingBottom: 8}]}>
               <PreviewableUserAvatar
@@ -256,7 +251,8 @@ let PostThreadItemLoaded = ({
                 did={post.author.did}
                 handle={post.author.handle}
                 avatar={post.author.avatar}
-                moderation={moderation.avatar}
+                moderation={moderation.ui('avatar')}
+                type={post.author.associated?.labeler ? 'labeler' : 'user'}
               />
             </View>
             <View style={styles.layoutContent}>
@@ -271,35 +267,12 @@ let PostThreadItemLoaded = ({
                     {sanitizeDisplayName(
                       post.author.displayName ||
                         sanitizeHandle(post.author.handle),
+                      moderation.ui('displayName'),
                     )}
                   </Text>
                 </Link>
               </View>
               <View style={styles.meta}>
-                {isAuthorMuted && (
-                  <View
-                    style={[
-                      pal.viewLight,
-                      {
-                        flexDirection: 'row',
-                        alignItems: 'center',
-                        gap: 4,
-                        borderRadius: 6,
-                        paddingHorizontal: 6,
-                        paddingVertical: 2,
-                        marginRight: 4,
-                      },
-                    ]}>
-                    <FontAwesomeIcon
-                      icon={['far', 'eye-slash']}
-                      size={12}
-                      color={pal.colors.textLight}
-                    />
-                    <Text type="sm-medium" style={pal.textLight}>
-                      Muted
-                    </Text>
-                  </View>
-                )}
                 <Link style={s.flex1} href={authorHref} title={authorTitle}>
                   <Text type="md" style={[pal.textLight]} numberOfLines={1}>
                     {sanitizeHandle(post.author.handle, '@')}
@@ -312,15 +285,16 @@ let PostThreadItemLoaded = ({
             )}
           </View>
           <View style={[s.pl10, s.pr10, s.pb10]}>
+            <LabelsOnMyPost post={post} />
             <ContentHider
-              moderation={moderation.content}
+              modui={moderation.ui('contentView')}
               ignoreMute
               style={styles.contentHider}
               childContainerStyle={styles.contentHiderChild}>
               <PostAlerts
-                moderation={moderation.content}
+                modui={moderation.ui('contentView')}
                 includeMute
-                style={styles.alert}
+                style={[a.pt_2xs, a.pb_sm]}
               />
               {richText?.text ? (
                 <View
@@ -329,27 +303,18 @@ let PostThreadItemLoaded = ({
                     styles.postTextLargeContainer,
                   ]}>
                   <RichText
-                    type="post-text-lg"
-                    richText={richText}
-                    lineHeight={1.3}
-                    style={s.flex1}
+                    enableTags
                     selectable
+                    value={richText}
+                    style={[a.flex_1, a.text_xl]}
+                    authorHandle={post.author.handle}
                   />
                 </View>
               ) : undefined}
               {post.embed && (
-                <ContentHider
-                  moderation={moderation.embed}
-                  moderationDecisions={moderation.decisions}
-                  ignoreMute={isEmbedByEmbedder(post.embed, post.author.did)}
-                  ignoreQuoteDecisions
-                  style={s.mb10}>
-                  <PostEmbeds
-                    embed={post.embed}
-                    moderation={moderation.embed}
-                    moderationDecisions={moderation.decisions}
-                  />
-                </ContentHider>
+                <View style={[a.pb_sm]}>
+                  <PostEmbeds embed={post.embed} moderation={moderation} />
+                </View>
               )}
             </ContentHider>
             <ExpandedPostDetails
@@ -357,9 +322,16 @@ let PostThreadItemLoaded = ({
               translatorUrl={translatorUrl}
               needsTranslation={needsTranslation}
             />
-            {hasEngagement ? (
+            {post.repostCount !== 0 || post.likeCount !== 0 ? (
+              // Show this section unless we're *sure* it has no engagement.
               <View style={[styles.expandedInfo, pal.border]}>
-                {post.repostCount ? (
+                {post.repostCount == null && post.likeCount == null && (
+                  // If we're still loading and not sure, assume this post has engagement.
+                  // This lets us avoid a layout shift for the common case (embedded post with likes/reposts).
+                  // TODO: embeds should include metrics to avoid us having to guess.
+                  <LoadingPlaceholder width={50} height={20} />
+                )}
+                {post.repostCount != null && post.repostCount !== 0 ? (
                   <Link
                     style={styles.expandedInfoItem}
                     href={repostsHref}
@@ -374,10 +346,8 @@ let PostThreadItemLoaded = ({
                       {pluralize(post.repostCount, 'repost')}
                     </Text>
                   </Link>
-                ) : (
-                  <></>
-                )}
-                {post.likeCount ? (
+                ) : null}
+                {post.likeCount != null && post.likeCount !== 0 ? (
                   <Link
                     style={styles.expandedInfoItem}
                     href={likesHref}
@@ -392,13 +362,9 @@ let PostThreadItemLoaded = ({
                       {pluralize(post.likeCount, 'like')}
                     </Text>
                   </Link>
-                ) : (
-                  <></>
-                )}
+                ) : null}
               </View>
-            ) : (
-              <></>
-            )}
+            ) : null}
             <View style={[s.pl10, s.pr10, s.pb5]}>
               <PostCtrls
                 big
@@ -406,6 +372,7 @@ let PostThreadItemLoaded = ({
                 record={record}
                 richText={richText}
                 onPressReply={onPressReply}
+                logContext="PostThreadItem"
               />
             </View>
           </View>
@@ -431,15 +398,13 @@ let PostThreadItemLoaded = ({
             testID={`postThreadItem-by-${post.author.handle}`}
             href={postHref}
             style={[pal.view]}
-            moderation={moderation.content}
+            modui={moderation.ui('contentList')}
             iconSize={isThreadedChild ? 26 : 38}
             iconStyles={
               isThreadedChild
                 ? {marginRight: 4}
                 : {marginLeft: 2, marginRight: 2}
             }>
-            <PostSandboxWarning />
-
             <View
               style={{
                 flexDirection: 'row',
@@ -454,7 +419,7 @@ let PostThreadItemLoaded = ({
                       styles.replyLine,
                       {
                         flexGrow: 1,
-                        backgroundColor: pal.colors.border,
+                        backgroundColor: pal.colors.replyLine,
                         marginBottom: 4,
                       },
                     ]}
@@ -483,7 +448,8 @@ let PostThreadItemLoaded = ({
                     did={post.author.did}
                     handle={post.author.handle}
                     avatar={post.author.avatar}
-                    moderation={moderation.avatar}
+                    moderation={moderation.ui('avatar')}
+                    type={post.author.associated?.labeler ? 'labeler' : 'user'}
                   />
 
                   {showChildReplyLine && (
@@ -492,7 +458,7 @@ let PostThreadItemLoaded = ({
                         styles.replyLine,
                         {
                           flexGrow: 1,
-                          backgroundColor: pal.colors.border,
+                          backgroundColor: pal.colors.replyLine,
                           marginTop: 4,
                         },
                       ]}
@@ -509,28 +475,30 @@ let PostThreadItemLoaded = ({
                 }>
                 <PostMeta
                   author={post.author}
+                  moderation={moderation}
                   authorHasWarning={!!post.author.labels?.length}
                   timestamp={post.indexedAt}
                   postHref={postHref}
                   showAvatar={isThreadedChild}
-                  avatarModeration={moderation.avatar}
+                  avatarModeration={moderation.ui('avatar')}
                   avatarSize={28}
                   displayNameType="md-bold"
                   displayNameStyle={isThreadedChild && s.ml2}
                   style={isThreadedChild && s.mb2}
                 />
+                <LabelsOnMyPost post={post} />
                 <PostAlerts
-                  moderation={moderation.content}
-                  style={styles.alert}
+                  modui={moderation.ui('contentList')}
+                  style={[a.pt_xs, a.pb_sm]}
                 />
                 {richText?.text ? (
                   <View style={styles.postTextContainer}>
                     <RichText
-                      type="post-text"
-                      richText={richText}
-                      style={[pal.text, s.flex1]}
-                      lineHeight={1.3}
+                      enableTags
+                      value={richText}
+                      style={[a.flex_1, a.text_md]}
                       numberOfLines={limitLines ? MAX_POST_LINES : undefined}
+                      authorHandle={post.author.handle}
                     />
                   </View>
                 ) : undefined}
@@ -543,24 +511,16 @@ let PostThreadItemLoaded = ({
                   />
                 ) : undefined}
                 {post.embed && (
-                  <ContentHider
-                    style={styles.contentHider}
-                    moderation={moderation.embed}
-                    moderationDecisions={moderation.decisions}
-                    ignoreMute={isEmbedByEmbedder(post.embed, post.author.did)}
-                    ignoreQuoteDecisions>
-                    <PostEmbeds
-                      embed={post.embed}
-                      moderation={moderation.embed}
-                      moderationDecisions={moderation.decisions}
-                    />
-                  </ContentHider>
+                  <View style={[a.pb_xs]}>
+                    <PostEmbeds embed={post.embed} moderation={moderation} />
+                  </View>
                 )}
                 <PostCtrls
                   post={post}
                   record={record}
                   richText={richText}
                   onPressReply={onPressReply}
+                  logContext="PostThreadItem"
                 />
               </View>
             </View>
@@ -578,7 +538,7 @@ let PostThreadItemLoaded = ({
                 title={itemTitle}
                 noFeedback>
                 <Text type="sm-medium" style={pal.textLight}>
-                  More
+                  <Trans>More</Trans>
                 </Text>
                 <FontAwesomeIcon
                   icon="angle-right"
@@ -621,7 +581,6 @@ function PostOuterWrapper({
     return (
       <View
         style={[
-          pal.view,
           pal.border,
           styles.cursor,
           {
@@ -649,7 +608,6 @@ function PostOuterWrapper({
     <View
       style={[
         styles.outer,
-        pal.view,
         pal.border,
         showParentReplyLine && hasPrecedingItem && styles.noTopBorder,
         styles.cursor,
diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx
index 2f1c0d37b..47e46eb0c 100644
--- a/src/view/com/post/Post.tsx
+++ b/src/view/com/post/Post.tsx
@@ -4,7 +4,7 @@ import {
   AppBskyFeedDefs,
   AppBskyFeedPost,
   AtUri,
-  PostModeration,
+  ModerationDecision,
   RichText as RichTextAPI,
 } from '@atproto/api'
 import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
@@ -14,10 +14,11 @@ import {UserInfoText} from '../util/UserInfoText'
 import {PostMeta} from '../util/PostMeta'
 import {PostEmbeds} from '../util/post-embeds'
 import {PostCtrls} from '../util/post-ctrls/PostCtrls'
-import {ContentHider} from '../util/moderation/ContentHider'
-import {PostAlerts} from '../util/moderation/PostAlerts'
+import {ContentHider} from '../../../components/moderation/ContentHider'
+import {PostAlerts} from '../../../components/moderation/PostAlerts'
+import {LabelsOnMyPost} from '../../../components/moderation/LabelsOnMe'
 import {Text} from '../util/text/Text'
-import {RichText} from '../util/text/RichText'
+import {RichText} from '#/components/RichText'
 import {PreviewableUserAvatar} from '../util/UserAvatar'
 import {s, colors} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
@@ -29,6 +30,7 @@ import {useComposerControls} from '#/state/shell/composer'
 import {Shadow, usePostShadow, POST_TOMBSTONE} from '#/state/cache/post-shadow'
 import {Trans, msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
+import {atoms as a} from '#/alf'
 
 export function Post({
   post,
@@ -92,7 +94,7 @@ function PostInner({
   post: Shadow<AppBskyFeedDefs.PostView>
   record: AppBskyFeedPost.Record
   richText: RichTextAPI
-  moderation: PostModeration
+  moderation: ModerationDecision
   showReplyLine?: boolean
   style?: StyleProp<ViewStyle>
 }) {
@@ -116,11 +118,7 @@ function PostInner({
         uri: post.uri,
         cid: post.cid,
         text: record.text,
-        author: {
-          handle: post.author.handle,
-          displayName: post.author.displayName,
-          avatar: post.author.avatar,
-        },
+        author: post.author,
         embed: post.embed,
         moderation,
       },
@@ -132,7 +130,7 @@ function PostInner({
   }, [setLimitLines])
 
   return (
-    <Link href={itemHref} style={[styles.outer, pal.view, pal.border, style]}>
+    <Link href={itemHref} style={[styles.outer, pal.border, style]}>
       {showReplyLine && <View style={styles.replyLine} />}
       <View style={styles.layout}>
         <View style={styles.layoutAvi}>
@@ -141,12 +139,14 @@ function PostInner({
             did={post.author.did}
             handle={post.author.handle}
             avatar={post.author.avatar}
-            moderation={moderation.avatar}
+            moderation={moderation.ui('avatar')}
+            type={post.author.associated?.labeler ? 'labeler' : 'user'}
           />
         </View>
         <View style={styles.layoutContent}>
           <PostMeta
             author={post.author}
+            moderation={moderation}
             authorHasWarning={!!post.author.labels?.length}
             timestamp={post.indexedAt}
             postHref={itemHref}
@@ -175,20 +175,24 @@ function PostInner({
               </Text>
             </View>
           )}
+          <LabelsOnMyPost post={post} />
           <ContentHider
-            moderation={moderation.content}
+            modui={moderation.ui('contentView')}
             style={styles.contentHider}
             childContainerStyle={styles.contentHiderChild}>
-            <PostAlerts moderation={moderation.content} style={styles.alert} />
+            <PostAlerts
+              modui={moderation.ui('contentView')}
+              style={[a.py_xs]}
+            />
             {richText.text ? (
               <View style={styles.postTextContainer}>
                 <RichText
+                  enableTags
                   testID="postText"
-                  type="post-text"
-                  richText={richText}
-                  lineHeight={1.3}
+                  value={richText}
                   numberOfLines={limitLines ? MAX_POST_LINES : undefined}
-                  style={s.flex1}
+                  style={[a.flex_1, a.text_md]}
+                  authorHandle={post.author.handle}
                 />
               </View>
             ) : undefined}
@@ -201,17 +205,7 @@ function PostInner({
               />
             ) : undefined}
             {post.embed ? (
-              <ContentHider
-                moderation={moderation.embed}
-                moderationDecisions={moderation.decisions}
-                ignoreQuoteDecisions
-                style={styles.contentHider}>
-                <PostEmbeds
-                  embed={post.embed}
-                  moderation={moderation.embed}
-                  moderationDecisions={moderation.decisions}
-                />
-              </ContentHider>
+              <PostEmbeds embed={post.embed} moderation={moderation} />
             ) : null}
           </ContentHider>
           <PostCtrls
@@ -219,6 +213,7 @@ function PostInner({
             record={record}
             richText={richText}
             onPressReply={onPressReply}
+            logContext="Post"
           />
         </View>
       </View>
diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx
index 54d8aa224..8afcce94f 100644
--- a/src/view/com/posts/Feed.tsx
+++ b/src/view/com/posts/Feed.tsx
@@ -32,6 +32,8 @@ import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {DiscoverFallbackHeader} from './DiscoverFallbackHeader'
 import {FALLBACK_MARKER_POST} from '#/lib/api/feed/home'
+import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
+import {logEvent} from '#/lib/statsig/statsig'
 
 const LOADING_ITEM = {_reactKey: '__loading__'}
 const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
@@ -84,9 +86,11 @@ let Feed = ({
   const {_} = useLingui()
   const queryClient = useQueryClient()
   const {currentAccount} = useSession()
+  const initialNumToRender = useInitialNumToRender()
   const [isPTRing, setIsPTRing] = React.useState(false)
   const checkForNewRef = React.useRef<(() => void) | null>(null)
   const lastFetchRef = React.useRef<number>(Date.now())
+  const feedType = feed.split('|')[0]
 
   const opts = React.useMemo(
     () => ({enabled, ignoreFilterFor}),
@@ -211,6 +215,10 @@ let Feed = ({
 
   const onRefresh = React.useCallback(async () => {
     track('Feed:onRefresh')
+    logEvent('feed:refresh', {
+      feedType: feedType,
+      reason: 'pull-to-refresh',
+    })
     setIsPTRing(true)
     try {
       await refetch()
@@ -219,18 +227,30 @@ let Feed = ({
       logger.error('Failed to refresh posts feed', {message: err})
     }
     setIsPTRing(false)
-  }, [refetch, track, setIsPTRing, onHasNew])
+  }, [refetch, track, setIsPTRing, onHasNew, feedType])
 
   const onEndReached = React.useCallback(async () => {
     if (isFetching || !hasNextPage || isError) return
 
+    logEvent('feed:endReached', {
+      feedType: feedType,
+      itemCount: feedItems.length,
+    })
     track('Feed:onEndReached')
     try {
       await fetchNextPage()
     } catch (err) {
       logger.error('Failed to load more posts', {message: err})
     }
-  }, [isFetching, hasNextPage, isError, fetchNextPage, track])
+  }, [
+    isFetching,
+    hasNextPage,
+    isError,
+    fetchNextPage,
+    track,
+    feedType,
+    feedItems.length,
+  ])
 
   const onPressTryAgain = React.useCallback(() => {
     refetch()
@@ -327,6 +347,8 @@ let Feed = ({
         desktopFixedHeight={
           desktopFixedHeightOffset ? desktopFixedHeightOffset : true
         }
+        initialNumToRender={initialNumToRender}
+        windowSize={11}
       />
     </View>
   )
diff --git a/src/view/com/posts/FeedErrorMessage.tsx b/src/view/com/posts/FeedErrorMessage.tsx
index 6d99c32f1..d4ca38d07 100644
--- a/src/view/com/posts/FeedErrorMessage.tsx
+++ b/src/view/com/posts/FeedErrorMessage.tsx
@@ -9,13 +9,13 @@ import {usePalette} from 'lib/hooks/usePalette'
 import {useNavigation} from '@react-navigation/native'
 import {NavigationProp} from 'lib/routes/types'
 import {logger} from '#/logger'
-import {useModalControls} from '#/state/modals'
 import {msg as msgLingui, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {FeedDescriptor} from '#/state/queries/post-feed'
 import {EmptyState} from '../util/EmptyState'
 import {cleanError} from '#/lib/strings/errors'
 import {useRemoveFeedMutation} from '#/state/queries/preferences'
+import * as Prompt from '#/components/Prompt'
 
 export enum KnownError {
   Block = 'Block',
@@ -46,7 +46,7 @@ export function FeedErrorMessage({
   if (
     typeof knownError !== 'undefined' &&
     knownError !== KnownError.Unknown &&
-    feedDesc.startsWith('feedgen')
+    (feedDesc.startsWith('feedgen') || knownError === KnownError.FeedNSFPublic)
   ) {
     return (
       <FeedgenErrorMessage
@@ -118,35 +118,29 @@ function FeedgenErrorMessage({
   )
   const [_, uri] = feedDesc.split('|')
   const [ownerDid] = safeParseFeedgenUri(uri)
-  const {openModal, closeModal} = useModalControls()
+  const removePromptControl = Prompt.usePromptControl()
   const {mutateAsync: removeFeed} = useRemoveFeedMutation()
 
   const onViewProfile = React.useCallback(() => {
     navigation.navigate('Profile', {name: ownerDid})
   }, [navigation, ownerDid])
 
+  const onPressRemoveFeed = React.useCallback(() => {
+    removePromptControl.open()
+  }, [removePromptControl])
+
   const onRemoveFeed = React.useCallback(async () => {
-    openModal({
-      name: 'confirm',
-      title: _l(msgLingui`Remove feed`),
-      message: _l(msgLingui`Remove this feed from your saved feeds?`),
-      async onPressConfirm() {
-        try {
-          await removeFeed({uri})
-        } catch (err) {
-          Toast.show(
-            _l(
-              msgLingui`There was an an issue removing this feed. Please check your internet connection and try again.`,
-            ),
-          )
-          logger.error('Failed to remove feed', {message: err})
-        }
-      },
-      onPressCancel() {
-        closeModal()
-      },
-    })
-  }, [openModal, closeModal, uri, removeFeed, _l])
+    try {
+      await removeFeed({uri})
+    } catch (err) {
+      Toast.show(
+        _l(
+          msgLingui`There was an an issue removing this feed. Please check your internet connection and try again.`,
+        ),
+      )
+      logger.error('Failed to remove feed', {message: err})
+    }
+  }, [uri, removeFeed, _l])
 
   const cta = React.useMemo(() => {
     switch (knownError) {
@@ -179,27 +173,38 @@ function FeedgenErrorMessage({
   }, [knownError, onViewProfile, onRemoveFeed, _l])
 
   return (
-    <View
-      style={[
-        pal.border,
-        pal.viewLight,
-        {
-          borderTopWidth: 1,
-          paddingHorizontal: 20,
-          paddingVertical: 18,
-          gap: 12,
-        },
-      ]}>
-      <Text style={pal.text}>{msg}</Text>
+    <>
+      <View
+        style={[
+          pal.border,
+          pal.viewLight,
+          {
+            borderTopWidth: 1,
+            paddingHorizontal: 20,
+            paddingVertical: 18,
+            gap: 12,
+          },
+        ]}>
+        <Text style={pal.text}>{msg}</Text>
 
-      {rawError?.message && (
-        <Text style={pal.textLight}>
-          <Trans>Message from server: {rawError.message}</Trans>
-        </Text>
-      )}
+        {rawError?.message && (
+          <Text style={pal.textLight}>
+            <Trans>Message from server: {rawError.message}</Trans>
+          </Text>
+        )}
 
-      {cta}
-    </View>
+        {cta}
+      </View>
+
+      <Prompt.Basic
+        control={removePromptControl}
+        title={_l(msgLingui`Remove feed?`)}
+        description={_l(msgLingui`Remove this feed from your saved feeds`)}
+        onConfirm={onPressRemoveFeed}
+        confirmButtonCta={_l(msgLingui`Remove`)}
+        confirmButtonColor="negative"
+      />
+    </>
   )
 }
 
@@ -235,6 +240,9 @@ function detectKnownError(
   if (typeof error !== 'string') {
     error = error.toString()
   }
+  if (error.includes(KnownError.FeedNSFPublic)) {
+    return KnownError.FeedNSFPublic
+  }
   if (!feedDesc.startsWith('feedgen')) {
     return KnownError.Unknown
   }
@@ -258,8 +266,5 @@ function detectKnownError(
   if (error.includes('feed provided an invalid response')) {
     return KnownError.FeedgenBadResponse
   }
-  if (error.includes(KnownError.FeedNSFPublic)) {
-    return KnownError.FeedNSFPublic
-  }
   return KnownError.FeedgenUnknown
 }
diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx
index 920409ec6..0fbcc4a13 100644
--- a/src/view/com/posts/FeedItem.tsx
+++ b/src/view/com/posts/FeedItem.tsx
@@ -4,7 +4,7 @@ import {
   AppBskyFeedDefs,
   AppBskyFeedPost,
   AtUri,
-  PostModeration,
+  ModerationDecision,
   RichText as RichTextAPI,
 } from '@atproto/api'
 import {
@@ -18,25 +18,24 @@ import {UserInfoText} from '../util/UserInfoText'
 import {PostMeta} from '../util/PostMeta'
 import {PostCtrls} from '../util/post-ctrls/PostCtrls'
 import {PostEmbeds} from '../util/post-embeds'
-import {ContentHider} from '../util/moderation/ContentHider'
-import {PostAlerts} from '../util/moderation/PostAlerts'
-import {RichText} from '../util/text/RichText'
-import {PostSandboxWarning} from '../util/PostSandboxWarning'
+import {ContentHider} from '#/components/moderation/ContentHider'
+import {PostAlerts} from '../../../components/moderation/PostAlerts'
+import {LabelsOnMyPost} from '../../../components/moderation/LabelsOnMe'
+import {RichText} from '#/components/RichText'
 import {PreviewableUserAvatar} from '../util/UserAvatar'
 import {s} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
 import {sanitizeDisplayName} from 'lib/strings/display-names'
 import {sanitizeHandle} from 'lib/strings/handles'
 import {makeProfileLink} from 'lib/routes/links'
-import {isEmbedByEmbedder} from 'lib/embeds'
 import {MAX_POST_LINES} from 'lib/constants'
 import {countLines} from 'lib/strings/helpers'
 import {useComposerControls} from '#/state/shell/composer'
 import {Shadow, usePostShadow, POST_TOMBSTONE} from '#/state/cache/post-shadow'
 import {FeedNameText} from '../util/FeedInfoText'
-import {useSession} from '#/state/session'
 import {Trans, msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
+import {atoms as a} from '#/alf'
 
 export function FeedItem({
   post,
@@ -50,7 +49,7 @@ export function FeedItem({
   post: AppBskyFeedDefs.PostView
   record: AppBskyFeedPost.Record
   reason: AppBskyFeedDefs.ReasonRepost | ReasonFeedSource | undefined
-  moderation: PostModeration
+  moderation: ModerationDecision
   isThreadChild?: boolean
   isThreadLastChild?: boolean
   isThreadParent?: boolean
@@ -70,6 +69,8 @@ export function FeedItem({
   if (richText && moderation) {
     return (
       <FeedItemInner
+        // Safeguard from clobbering per-post state below:
+        key={postShadowed.uri}
         post={postShadowed}
         record={record}
         reason={reason}
@@ -98,7 +99,7 @@ let FeedItemInner = ({
   record: AppBskyFeedPost.Record
   reason: AppBskyFeedDefs.ReasonRepost | ReasonFeedSource | undefined
   richText: RichTextAPI
-  moderation: PostModeration
+  moderation: ModerationDecision
   isThreadChild?: boolean
   isThreadLastChild?: boolean
   isThreadParent?: boolean
@@ -106,14 +107,10 @@ let FeedItemInner = ({
   const {openComposer} = useComposerControls()
   const pal = usePalette('default')
   const {_} = useLingui()
-  const {currentAccount} = useSession()
   const href = useMemo(() => {
     const urip = new AtUri(post.uri)
     return makeProfileLink(post.author, 'post', urip.rkey)
   }, [post.uri, post.author])
-  const isModeratedPost =
-    moderation.decisions.post.cause?.type === 'label' &&
-    moderation.decisions.post.cause.label.src !== currentAccount?.did
 
   const replyAuthorDid = useMemo(() => {
     if (!record?.reply) {
@@ -129,11 +126,7 @@ let FeedItemInner = ({
         uri: post.uri,
         cid: post.cid,
         text: record.text || '',
-        author: {
-          handle: post.author.handle,
-          displayName: post.author.displayName,
-          avatar: post.author.avatar,
-        },
+        author: post.author,
         embed: post.embed,
         moderation,
       },
@@ -142,12 +135,11 @@ let FeedItemInner = ({
 
   const outerStyles = [
     styles.outer,
-    pal.view,
     {
       borderColor: pal.colors.border,
       paddingBottom:
         isThreadLastChild || (!isThreadChild && !isThreadParent)
-          ? 6
+          ? 8
           : undefined,
     },
     isThreadChild ? styles.outerSmallTop : undefined,
@@ -160,8 +152,6 @@ let FeedItemInner = ({
       href={href}
       noFeedback
       accessible={false}>
-      <PostSandboxWarning />
-
       <View style={{flexDirection: 'row', gap: 10, paddingLeft: 8}}>
         <View style={{width: 52}}>
           {isThreadChild && (
@@ -230,6 +220,7 @@ let FeedItemInner = ({
                     numberOfLines={1}
                     text={sanitizeDisplayName(
                       reason.by.displayName || sanitizeHandle(reason.by.handle),
+                      moderation.ui('displayName'),
                     )}
                     href={makeProfileLink(reason.by)}
                   />
@@ -247,7 +238,8 @@ let FeedItemInner = ({
             did={post.author.did}
             handle={post.author.handle}
             avatar={post.author.avatar}
-            moderation={moderation.avatar}
+            moderation={moderation.ui('avatar')}
+            type={post.author.associated?.labeler ? 'labeler' : 'user'}
           />
           {isThreadParent && (
             <View
@@ -265,6 +257,7 @@ let FeedItemInner = ({
         <View style={styles.layoutContent}>
           <PostMeta
             author={post.author}
+            moderation={moderation}
             authorHasWarning={!!post.author.labels?.length}
             timestamp={post.indexedAt}
             postHref={href}
@@ -296,6 +289,7 @@ let FeedItemInner = ({
               </Text>
             </View>
           )}
+          <LabelsOnMyPost post={post} />
           <PostContent
             moderation={moderation}
             richText={richText}
@@ -307,9 +301,7 @@ let FeedItemInner = ({
             record={record}
             richText={richText}
             onPressReply={onPressReply}
-            showAppealLabelItem={
-              post.author.did === currentAccount?.did && isModeratedPost
-            }
+            logContext="FeedItem"
           />
         </View>
       </View>
@@ -324,7 +316,7 @@ let PostContent = ({
   postEmbed,
   postAuthor,
 }: {
-  moderation: PostModeration
+  moderation: ModerationDecision
   richText: RichTextAPI
   postEmbed: AppBskyFeedDefs.PostView['embed']
   postAuthor: AppBskyFeedDefs.PostView['author']
@@ -342,19 +334,19 @@ let PostContent = ({
   return (
     <ContentHider
       testID="contentHider-post"
-      moderation={moderation.content}
+      modui={moderation.ui('contentList')}
       ignoreMute
       childContainerStyle={styles.contentHiderChild}>
-      <PostAlerts moderation={moderation.content} style={styles.alert} />
+      <PostAlerts modui={moderation.ui('contentList')} style={[a.py_xs]} />
       {richText.text ? (
         <View style={styles.postTextContainer}>
           <RichText
+            enableTags
             testID="postText"
-            type="post-text"
-            richText={richText}
-            lineHeight={1.3}
+            value={richText}
             numberOfLines={limitLines ? MAX_POST_LINES : undefined}
-            style={s.flex1}
+            style={[a.flex_1, a.text_md]}
+            authorHandle={postAuthor.handle}
           />
         </View>
       ) : undefined}
@@ -367,19 +359,9 @@ let PostContent = ({
         />
       ) : undefined}
       {postEmbed ? (
-        <ContentHider
-          testID="contentHider-embed"
-          moderation={moderation.embed}
-          moderationDecisions={moderation.decisions}
-          ignoreMute={isEmbedByEmbedder(postEmbed, postAuthor.did)}
-          ignoreQuoteDecisions
-          style={styles.embed}>
-          <PostEmbeds
-            embed={postEmbed}
-            moderation={moderation.embed}
-            moderationDecisions={moderation.decisions}
-          />
-        </ContentHider>
+        <View style={[a.pb_sm]}>
+          <PostEmbeds embed={postEmbed} moderation={moderation} />
+        </View>
       ) : null}
     </ContentHider>
   )
diff --git a/src/view/com/posts/FeedSlice.tsx b/src/view/com/posts/FeedSlice.tsx
index 84edee4a1..49e48aa20 100644
--- a/src/view/com/posts/FeedSlice.tsx
+++ b/src/view/com/posts/FeedSlice.tsx
@@ -78,11 +78,7 @@ function ViewFullThread({slice}: {slice: FeedPostSlice}) {
   }, [slice.rootUri])
 
   return (
-    <Link
-      style={[pal.view, styles.viewFullThread]}
-      href={itemHref}
-      asAnchor
-      noFeedback>
+    <Link style={[styles.viewFullThread]} href={itemHref} asAnchor noFeedback>
       <View style={styles.viewFullThreadDots}>
         <Svg width="4" height="40">
           <Line
diff --git a/src/view/com/profile/FollowButton.tsx b/src/view/com/profile/FollowButton.tsx
index 9cc635b66..7b090ffeb 100644
--- a/src/view/com/profile/FollowButton.tsx
+++ b/src/view/com/profile/FollowButton.tsx
@@ -13,13 +13,18 @@ export function FollowButton({
   followedType = 'default',
   profile,
   labelStyle,
+  logContext,
 }: {
   unfollowedType?: ButtonType
   followedType?: ButtonType
   profile: Shadow<AppBskyActorDefs.ProfileViewBasic>
   labelStyle?: StyleProp<TextStyle>
+  logContext: 'ProfileCard'
 }) {
-  const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(profile)
+  const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(
+    profile,
+    logContext,
+  )
   const {_} = useLingui()
 
   const onPressFollow = async () => {
diff --git a/src/view/com/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx
index 266adc51d..235139fff 100644
--- a/src/view/com/profile/ProfileCard.tsx
+++ b/src/view/com/profile/ProfileCard.tsx
@@ -3,7 +3,8 @@ import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
 import {
   AppBskyActorDefs,
   moderateProfile,
-  ProfileModeration,
+  ModerationCause,
+  ModerationDecision,
 } from '@atproto/api'
 import {Link} from '../util/Link'
 import {Text} from '../util/text/Text'
@@ -14,16 +15,13 @@ import {FollowButton} from './FollowButton'
 import {sanitizeDisplayName} from 'lib/strings/display-names'
 import {sanitizeHandle} from 'lib/strings/handles'
 import {makeProfileLink} from 'lib/routes/links'
-import {
-  describeModerationCause,
-  getProfileModerationCauses,
-  getModerationCauseKey,
-} from 'lib/moderation'
+import {getModerationCauseKey, isJustAMute} from 'lib/moderation'
 import {Shadow} from '#/state/cache/types'
 import {useModerationOpts} from '#/state/queries/preferences'
 import {useProfileShadow} from '#/state/cache/profile-shadow'
 import {useSession} from '#/state/session'
 import {Trans} from '@lingui/macro'
+import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
 
 export function ProfileCard({
   testID,
@@ -33,6 +31,7 @@ export function ProfileCard({
   noBorder,
   followers,
   renderButton,
+  onPress,
   style,
 }: {
   testID?: string
@@ -44,20 +43,19 @@ export function ProfileCard({
   renderButton?: (
     profile: Shadow<AppBskyActorDefs.ProfileViewBasic>,
   ) => React.ReactNode
+  onPress?: () => void
   style?: StyleProp<ViewStyle>
 }) {
   const pal = usePalette('default')
   const profile = useProfileShadow(profileUnshadowed)
   const moderationOpts = useModerationOpts()
+  const isLabeler = profile?.associated?.labeler
   if (!moderationOpts) {
     return null
   }
   const moderation = moderateProfile(profile, moderationOpts)
-  if (
-    !noModFilter &&
-    moderation.account.filter &&
-    moderation.account.cause?.type !== 'muted'
-  ) {
+  const modui = moderation.ui('profileList')
+  if (!noModFilter && modui.filter && !isJustAMute(modui)) {
     return null
   }
 
@@ -73,6 +71,7 @@ export function ProfileCard({
       ]}
       href={makeProfileLink(profile)}
       title={profile.handle}
+      onBeforePress={onPress}
       asAnchor
       anchorNoUnderline>
       <View style={styles.layout}>
@@ -80,7 +79,8 @@ export function ProfileCard({
           <UserAvatar
             size={40}
             avatar={profile.avatar}
-            moderation={moderation.avatar}
+            moderation={moderation.ui('avatar')}
+            type={isLabeler ? 'labeler' : 'user'}
           />
         </View>
         <View style={styles.layoutContent}>
@@ -91,7 +91,7 @@ export function ProfileCard({
             lineHeight={1.2}>
             {sanitizeDisplayName(
               profile.displayName || sanitizeHandle(profile.handle),
-              moderation.profile,
+              moderation.ui('displayName'),
             )}
           </Text>
           <Text type="md" style={[pal.textLight]} numberOfLines={1}>
@@ -103,7 +103,7 @@ export function ProfileCard({
           />
           {!!profile.viewer?.followedBy && <View style={s.flexRow} />}
         </View>
-        {renderButton ? (
+        {renderButton && !isLabeler ? (
           <View style={styles.layoutButton}>{renderButton(profile)}</View>
         ) : undefined}
       </View>
@@ -119,17 +119,17 @@ export function ProfileCard({
   )
 }
 
-function ProfileCardPills({
+export function ProfileCardPills({
   followedBy,
   moderation,
 }: {
   followedBy: boolean
-  moderation: ProfileModeration
+  moderation: ModerationDecision
 }) {
   const pal = usePalette('default')
 
-  const causes = getProfileModerationCauses(moderation)
-  if (!followedBy && !causes.length) {
+  const modui = moderation.ui('profileList')
+  if (!followedBy && !modui.inform && !modui.alert) {
     return null
   }
 
@@ -142,19 +142,41 @@ function ProfileCardPills({
           </Text>
         </View>
       )}
-      {causes.map(cause => {
-        const desc = describeModerationCause(cause, 'account')
-        return (
-          <View
-            style={[s.mt5, pal.btn, styles.pill]}
-            key={getModerationCauseKey(cause)}>
-            <Text type="xs" style={pal.text}>
-              {cause?.type === 'label' ? 'âš ' : ''}
-              {desc.name}
-            </Text>
-          </View>
-        )
-      })}
+      {modui.alerts.map(alert => (
+        <ProfileCardPillModerationCause
+          key={getModerationCauseKey(alert)}
+          cause={alert}
+          severity="alert"
+        />
+      ))}
+      {modui.informs.map(inform => (
+        <ProfileCardPillModerationCause
+          key={getModerationCauseKey(inform)}
+          cause={inform}
+          severity="inform"
+        />
+      ))}
+    </View>
+  )
+}
+
+function ProfileCardPillModerationCause({
+  cause,
+  severity,
+}: {
+  cause: ModerationCause
+  severity: 'alert' | 'inform'
+}) {
+  const pal = usePalette('default')
+  const {name} = useModerationCauseDescription(cause)
+  return (
+    <View
+      style={[s.mt5, pal.btn, styles.pill]}
+      key={getModerationCauseKey(cause)}>
+      <Text type="xs" style={pal.text}>
+        {severity === 'alert' ? 'âš  ' : ''}
+        {name}
+      </Text>
     </View>
   )
 }
@@ -177,7 +199,7 @@ function FollowersList({
         f,
         mod: moderateProfile(f, moderationOpts),
       }))
-      .filter(({mod}) => !mod.account.filter)
+      .filter(({mod}) => !mod.ui('profileList').filter)
   }, [followers, moderationOpts])
 
   if (!followersWithMods?.length) {
@@ -199,7 +221,12 @@ function FollowersList({
       {followersWithMods.slice(0, 3).map(({f, mod}) => (
         <View key={f.did} style={styles.followedByAviContainer}>
           <View style={[styles.followedByAvi, pal.view]}>
-            <UserAvatar avatar={f.avatar} size={32} moderation={mod.avatar} />
+            <UserAvatar
+              avatar={f.avatar}
+              size={32}
+              moderation={mod.ui('avatar')}
+              type={f.associated?.labeler ? 'labeler' : 'user'}
+            />
           </View>
         </View>
       ))}
@@ -212,11 +239,13 @@ export function ProfileCardWithFollowBtn({
   noBg,
   noBorder,
   followers,
+  onPress,
 }: {
   profile: AppBskyActorDefs.ProfileViewBasic
   noBg?: boolean
   noBorder?: boolean
   followers?: AppBskyActorDefs.ProfileView[] | undefined
+  onPress?: () => void
 }) {
   const {currentAccount} = useSession()
   const isMe = profile.did === currentAccount?.did
@@ -230,8 +259,11 @@ export function ProfileCardWithFollowBtn({
       renderButton={
         isMe
           ? undefined
-          : profileShadow => <FollowButton profile={profileShadow} />
+          : profileShadow => (
+              <FollowButton profile={profileShadow} logContext="ProfileCard" />
+            )
       }
+      onPress={onPress}
     />
   )
 }
diff --git a/src/view/com/profile/ProfileFollowers.tsx b/src/view/com/profile/ProfileFollowers.tsx
index 411ae6c17..b11a33f27 100644
--- a/src/view/com/profile/ProfileFollowers.tsx
+++ b/src/view/com/profile/ProfileFollowers.tsx
@@ -1,39 +1,66 @@
 import React from 'react'
-import {ActivityIndicator, StyleSheet, View} from 'react-native'
 import {AppBskyActorDefs as ActorDefs} from '@atproto/api'
-import {CenteredView} from '../util/Views'
-import {LoadingScreen} from '../util/LoadingScreen'
 import {List} from '../util/List'
-import {ErrorMessage} from '../util/error/ErrorMessage'
 import {ProfileCardWithFollowBtn} from './ProfileCard'
 import {useProfileFollowersQuery} from '#/state/queries/profile-followers'
 import {useResolveDidQuery} from '#/state/queries/resolve-uri'
 import {logger} from '#/logger'
 import {cleanError} from '#/lib/strings/errors'
+import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
+import {
+  ListFooter,
+  ListHeaderDesktop,
+  ListMaybePlaceholder,
+} from '#/components/Lists'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useSession} from 'state/session'
+import {View} from 'react-native'
+
+function renderItem({item}: {item: ActorDefs.ProfileViewBasic}) {
+  return <ProfileCardWithFollowBtn key={item.did} profile={item} />
+}
+
+function keyExtractor(item: ActorDefs.ProfileViewBasic) {
+  return item.did
+}
 
 export function ProfileFollowers({name}: {name: string}) {
+  const {_} = useLingui()
+  const initialNumToRender = useInitialNumToRender()
+  const {currentAccount} = useSession()
+
   const [isPTRing, setIsPTRing] = React.useState(false)
   const {
     data: resolvedDid,
+    isLoading: isDidLoading,
     error: resolveError,
-    isFetching: isFetchingDid,
   } = useResolveDidQuery(name)
   const {
     data,
+    isLoading: isFollowersLoading,
     isFetching,
-    isFetched,
     isFetchingNextPage,
     hasNextPage,
     fetchNextPage,
-    isError,
     error,
     refetch,
   } = useProfileFollowersQuery(resolvedDid)
 
+  const isError = React.useMemo(
+    () => !!resolveError || !!error,
+    [resolveError, error],
+  )
+
+  const isMe = React.useMemo(() => {
+    return resolvedDid === currentAccount?.did
+  }, [resolvedDid, currentAccount?.did])
+
   const followers = React.useMemo(() => {
     if (data?.pages) {
       return data.pages.flatMap(page => page.followers)
     }
+    return []
   }, [data])
 
   const onRefresh = React.useCallback(async () => {
@@ -47,7 +74,7 @@ export function ProfileFollowers({name}: {name: string}) {
   }, [refetch, setIsPTRing])
 
   const onEndReached = async () => {
-    if (isFetching || !hasNextPage || isError) return
+    if (isFetching || !hasNextPage || !!error) return
     try {
       await fetchNextPage()
     } catch (err) {
@@ -55,57 +82,38 @@ export function ProfileFollowers({name}: {name: string}) {
     }
   }
 
-  const renderItem = React.useCallback(
-    ({item}: {item: ActorDefs.ProfileViewBasic}) => (
-      <ProfileCardWithFollowBtn key={item.did} profile={item} />
-    ),
-    [],
-  )
-
-  if (isFetchingDid || !isFetched) {
-    return <LoadingScreen />
-  }
-
-  // error
-  // =
-  if (resolveError || isError) {
-    return (
-      <CenteredView>
-        <ErrorMessage
-          message={cleanError(resolveError || error)}
-          onPressTryAgain={onRefresh}
-        />
-      </CenteredView>
-    )
-  }
-
-  // loaded
-  // =
   return (
-    <List
-      data={followers}
-      keyExtractor={item => item.did}
-      refreshing={isPTRing}
-      onRefresh={onRefresh}
-      onEndReached={onEndReached}
-      renderItem={renderItem}
-      initialNumToRender={15}
-      // FIXME(dan)
-      // eslint-disable-next-line react/no-unstable-nested-components
-      ListFooterComponent={() => (
-        <View style={styles.footer}>
-          {(isFetching || isFetchingNextPage) && <ActivityIndicator />}
-        </View>
+    <View style={{flex: 1}}>
+      <ListMaybePlaceholder
+        isLoading={isDidLoading || isFollowersLoading}
+        isEmpty={followers.length < 1}
+        isError={isError}
+        emptyType="results"
+        emptyMessage={
+          isMe
+            ? _(msg`You do not have any followers.`)
+            : _(msg`This user doesn't have any followers.`)
+        }
+        errorMessage={cleanError(resolveError || error)}
+        onRetry={isError ? refetch : undefined}
+      />
+      {followers.length > 0 && (
+        <List
+          data={followers}
+          renderItem={renderItem}
+          keyExtractor={keyExtractor}
+          refreshing={isPTRing}
+          onRefresh={onRefresh}
+          onEndReached={onEndReached}
+          onEndReachedThreshold={4}
+          ListHeaderComponent={<ListHeaderDesktop title={_(msg`Followers`)} />}
+          ListFooterComponent={<ListFooter isFetching={isFetchingNextPage} />}
+          // @ts-ignore our .web version only -prf
+          desktopFixedHeight
+          initialNumToRender={initialNumToRender}
+          windowSize={11}
+        />
       )}
-      // @ts-ignore our .web version only -prf
-      desktopFixedHeight
-    />
+    </View>
   )
 }
-
-const styles = StyleSheet.create({
-  footer: {
-    height: 200,
-    paddingTop: 20,
-  },
-})
diff --git a/src/view/com/profile/ProfileFollows.tsx b/src/view/com/profile/ProfileFollows.tsx
index bd4af1081..d99e2b840 100644
--- a/src/view/com/profile/ProfileFollows.tsx
+++ b/src/view/com/profile/ProfileFollows.tsx
@@ -1,39 +1,65 @@
 import React from 'react'
-import {ActivityIndicator, StyleSheet, View} from 'react-native'
 import {AppBskyActorDefs as ActorDefs} from '@atproto/api'
-import {CenteredView} from '../util/Views'
-import {LoadingScreen} from '../util/LoadingScreen'
 import {List} from '../util/List'
-import {ErrorMessage} from '../util/error/ErrorMessage'
 import {ProfileCardWithFollowBtn} from './ProfileCard'
 import {useProfileFollowsQuery} from '#/state/queries/profile-follows'
 import {useResolveDidQuery} from '#/state/queries/resolve-uri'
 import {logger} from '#/logger'
 import {cleanError} from '#/lib/strings/errors'
+import {
+  ListFooter,
+  ListHeaderDesktop,
+  ListMaybePlaceholder,
+} from '#/components/Lists'
+import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
+import {useSession} from 'state/session'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+function renderItem({item}: {item: ActorDefs.ProfileViewBasic}) {
+  return <ProfileCardWithFollowBtn key={item.did} profile={item} />
+}
+
+function keyExtractor(item: ActorDefs.ProfileViewBasic) {
+  return item.did
+}
 
 export function ProfileFollows({name}: {name: string}) {
+  const {_} = useLingui()
+  const initialNumToRender = useInitialNumToRender()
+  const {currentAccount} = useSession()
+
   const [isPTRing, setIsPTRing] = React.useState(false)
   const {
     data: resolvedDid,
+    isLoading: isDidLoading,
     error: resolveError,
-    isFetching: isFetchingDid,
   } = useResolveDidQuery(name)
   const {
     data,
+    isLoading: isFollowsLoading,
     isFetching,
-    isFetched,
     isFetchingNextPage,
     hasNextPage,
     fetchNextPage,
-    isError,
     error,
     refetch,
   } = useProfileFollowsQuery(resolvedDid)
 
+  const isError = React.useMemo(
+    () => !!resolveError || !!error,
+    [resolveError, error],
+  )
+
+  const isMe = React.useMemo(() => {
+    return resolvedDid === currentAccount?.did
+  }, [resolvedDid, currentAccount?.did])
+
   const follows = React.useMemo(() => {
     if (data?.pages) {
       return data.pages.flatMap(page => page.follows)
     }
+    return []
   }, [data])
 
   const onRefresh = React.useCallback(async () => {
@@ -47,7 +73,7 @@ export function ProfileFollows({name}: {name: string}) {
   }, [refetch, setIsPTRing])
 
   const onEndReached = async () => {
-    if (isFetching || !hasNextPage || isError) return
+    if (isFetching || !hasNextPage || !!error) return
     try {
       await fetchNextPage()
     } catch (err) {
@@ -55,57 +81,38 @@ export function ProfileFollows({name}: {name: string}) {
     }
   }
 
-  const renderItem = React.useCallback(
-    ({item}: {item: ActorDefs.ProfileViewBasic}) => (
-      <ProfileCardWithFollowBtn key={item.did} profile={item} />
-    ),
-    [],
-  )
-
-  if (isFetchingDid || !isFetched) {
-    return <LoadingScreen />
-  }
-
-  // error
-  // =
-  if (resolveError || isError) {
-    return (
-      <CenteredView>
-        <ErrorMessage
-          message={cleanError(resolveError || error)}
-          onPressTryAgain={onRefresh}
-        />
-      </CenteredView>
-    )
-  }
-
-  // loaded
-  // =
   return (
-    <List
-      data={follows}
-      keyExtractor={item => item.did}
-      refreshing={isPTRing}
-      onRefresh={onRefresh}
-      onEndReached={onEndReached}
-      renderItem={renderItem}
-      initialNumToRender={15}
-      // FIXME(dan)
-      // eslint-disable-next-line react/no-unstable-nested-components
-      ListFooterComponent={() => (
-        <View style={styles.footer}>
-          {(isFetching || isFetchingNextPage) && <ActivityIndicator />}
-        </View>
+    <>
+      <ListMaybePlaceholder
+        isLoading={isDidLoading || isFollowsLoading}
+        isEmpty={follows.length < 1}
+        isError={isError}
+        emptyType="results"
+        emptyMessage={
+          isMe
+            ? _(msg`You are not following anyone.`)
+            : _(msg`This user isn't following anyone.`)
+        }
+        errorMessage={cleanError(resolveError || error)}
+        onRetry={isError ? refetch : undefined}
+      />
+      {follows.length > 0 && (
+        <List
+          data={follows}
+          renderItem={renderItem}
+          keyExtractor={keyExtractor}
+          refreshing={isPTRing}
+          onRefresh={onRefresh}
+          onEndReached={onEndReached}
+          onEndReachedThreshold={4}
+          ListHeaderComponent={<ListHeaderDesktop title={_(msg`Following`)} />}
+          ListFooterComponent={<ListFooter isFetching={isFetchingNextPage} />}
+          // @ts-ignore our .web version only -prf
+          desktopFixedHeight
+          initialNumToRender={initialNumToRender}
+          windowSize={11}
+        />
       )}
-      // @ts-ignore our .web version only -prf
-      desktopFixedHeight
-    />
+    </>
   )
 }
-
-const styles = StyleSheet.create({
-  footer: {
-    height: 200,
-    paddingTop: 20,
-  },
-})
diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx
deleted file mode 100644
index 8fd50fad6..000000000
--- a/src/view/com/profile/ProfileHeader.tsx
+++ /dev/null
@@ -1,784 +0,0 @@
-import React, {memo, useMemo} from 'react'
-import {
-  StyleSheet,
-  TouchableOpacity,
-  TouchableWithoutFeedback,
-  View,
-} from 'react-native'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {useNavigation} from '@react-navigation/native'
-import {useQueryClient} from '@tanstack/react-query'
-import {
-  AppBskyActorDefs,
-  ModerationOpts,
-  moderateProfile,
-  RichText as RichTextAPI,
-} from '@atproto/api'
-import {Trans, msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {NavigationProp} from 'lib/routes/types'
-import {isNative, isWeb} from 'platform/detection'
-import {BlurView} from '../util/BlurView'
-import * as Toast from '../util/Toast'
-import {LoadingPlaceholder} from '../util/LoadingPlaceholder'
-import {Text} from '../util/text/Text'
-import {ThemedText} from '../util/text/ThemedText'
-import {RichText} from '../util/text/RichText'
-import {UserAvatar} from '../util/UserAvatar'
-import {UserBanner} from '../util/UserBanner'
-import {ProfileHeaderAlerts} from '../util/moderation/ProfileHeaderAlerts'
-import {formatCount} from '../util/numeric/format'
-import {NativeDropdown, DropdownItem} from '../util/forms/NativeDropdown'
-import {Link} from '../util/Link'
-import {ProfileHeaderSuggestedFollows} from './ProfileHeaderSuggestedFollows'
-import {useModalControls} from '#/state/modals'
-import {useLightboxControls, ProfileImageLightbox} from '#/state/lightbox'
-import {
-  RQKEY as profileQueryKey,
-  useProfileMuteMutationQueue,
-  useProfileBlockMutationQueue,
-  useProfileFollowMutationQueue,
-} from '#/state/queries/profile'
-import {usePalette} from 'lib/hooks/usePalette'
-import {useAnalytics} from 'lib/analytics/analytics'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {BACK_HITSLOP} from 'lib/constants'
-import {isInvalidHandle, sanitizeHandle} from 'lib/strings/handles'
-import {makeProfileLink} from 'lib/routes/links'
-import {pluralize} from 'lib/strings/helpers'
-import {toShareUrl} from 'lib/strings/url-helpers'
-import {sanitizeDisplayName} from 'lib/strings/display-names'
-import {shareUrl} from 'lib/sharing'
-import {s, colors} from 'lib/styles'
-import {logger} from '#/logger'
-import {useSession} from '#/state/session'
-import {Shadow} from '#/state/cache/types'
-import {useRequireAuth} from '#/state/session'
-import {LabelInfo} from '../util/moderation/LabelInfo'
-import {useProfileShadow} from 'state/cache/profile-shadow'
-
-let ProfileHeaderLoading = (_props: {}): React.ReactNode => {
-  const pal = usePalette('default')
-  return (
-    <View style={pal.view}>
-      <LoadingPlaceholder width="100%" height={150} style={{borderRadius: 0}} />
-      <View
-        style={[pal.view, {borderColor: pal.colors.background}, styles.avi]}>
-        <LoadingPlaceholder width={80} height={80} style={styles.br40} />
-      </View>
-      <View style={styles.content}>
-        <View style={[styles.buttonsLine]}>
-          <LoadingPlaceholder width={167} height={31} style={styles.br50} />
-        </View>
-      </View>
-    </View>
-  )
-}
-ProfileHeaderLoading = memo(ProfileHeaderLoading)
-export {ProfileHeaderLoading}
-
-interface Props {
-  profile: AppBskyActorDefs.ProfileViewDetailed
-  descriptionRT: RichTextAPI | null
-  moderationOpts: ModerationOpts
-  hideBackButton?: boolean
-  isPlaceholderProfile?: boolean
-}
-
-let ProfileHeader = ({
-  profile: profileUnshadowed,
-  descriptionRT,
-  moderationOpts,
-  hideBackButton = false,
-  isPlaceholderProfile,
-}: Props): React.ReactNode => {
-  const profile: Shadow<AppBskyActorDefs.ProfileViewDetailed> =
-    useProfileShadow(profileUnshadowed)
-  const pal = usePalette('default')
-  const palInverted = usePalette('inverted')
-  const {currentAccount, hasSession} = useSession()
-  const requireAuth = useRequireAuth()
-  const {_} = useLingui()
-  const {openModal} = useModalControls()
-  const {openLightbox} = useLightboxControls()
-  const navigation = useNavigation<NavigationProp>()
-  const {track} = useAnalytics()
-  const invalidHandle = isInvalidHandle(profile.handle)
-  const {isDesktop} = useWebMediaQueries()
-  const [showSuggestedFollows, setShowSuggestedFollows] = React.useState(false)
-  const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(profile)
-  const [queueMute, queueUnmute] = useProfileMuteMutationQueue(profile)
-  const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile)
-  const queryClient = useQueryClient()
-  const moderation = useMemo(
-    () => moderateProfile(profile, moderationOpts),
-    [profile, moderationOpts],
-  )
-
-  const invalidateProfileQuery = React.useCallback(() => {
-    queryClient.invalidateQueries({
-      queryKey: profileQueryKey(profile.did),
-    })
-  }, [queryClient, profile.did])
-
-  const onPressBack = React.useCallback(() => {
-    if (navigation.canGoBack()) {
-      navigation.goBack()
-    } else {
-      navigation.navigate('Home')
-    }
-  }, [navigation])
-
-  const onPressAvi = React.useCallback(() => {
-    if (
-      profile.avatar &&
-      !(moderation.avatar.blur && moderation.avatar.noOverride)
-    ) {
-      openLightbox(new ProfileImageLightbox(profile))
-    }
-  }, [openLightbox, profile, moderation])
-
-  const onPressFollow = () => {
-    requireAuth(async () => {
-      try {
-        track('ProfileHeader:FollowButtonClicked')
-        await queueFollow()
-        Toast.show(
-          _(
-            msg`Following ${sanitizeDisplayName(
-              profile.displayName || profile.handle,
-            )}`,
-          ),
-        )
-      } catch (e: any) {
-        if (e?.name !== 'AbortError') {
-          logger.error('Failed to follow', {message: String(e)})
-          Toast.show(_(msg`There was an issue! ${e.toString()}`))
-        }
-      }
-    })
-  }
-
-  const onPressUnfollow = () => {
-    requireAuth(async () => {
-      try {
-        track('ProfileHeader:UnfollowButtonClicked')
-        await queueUnfollow()
-        Toast.show(
-          _(
-            msg`No longer following ${sanitizeDisplayName(
-              profile.displayName || profile.handle,
-            )}`,
-          ),
-        )
-      } catch (e: any) {
-        if (e?.name !== 'AbortError') {
-          logger.error('Failed to unfollow', {message: String(e)})
-          Toast.show(_(msg`There was an issue! ${e.toString()}`))
-        }
-      }
-    })
-  }
-
-  const onPressEditProfile = React.useCallback(() => {
-    track('ProfileHeader:EditProfileButtonClicked')
-    openModal({
-      name: 'edit-profile',
-      profile,
-    })
-  }, [track, openModal, profile])
-
-  const onPressShare = React.useCallback(() => {
-    track('ProfileHeader:ShareButtonClicked')
-    shareUrl(toShareUrl(makeProfileLink(profile)))
-  }, [track, profile])
-
-  const onPressAddRemoveLists = React.useCallback(() => {
-    track('ProfileHeader:AddToListsButtonClicked')
-    openModal({
-      name: 'user-add-remove-lists',
-      subject: profile.did,
-      handle: profile.handle,
-      displayName: profile.displayName || profile.handle,
-      onAdd: invalidateProfileQuery,
-      onRemove: invalidateProfileQuery,
-    })
-  }, [track, profile, openModal, invalidateProfileQuery])
-
-  const onPressMuteAccount = React.useCallback(async () => {
-    track('ProfileHeader:MuteAccountButtonClicked')
-    try {
-      await queueMute()
-      Toast.show(_(msg`Account muted`))
-    } catch (e: any) {
-      if (e?.name !== 'AbortError') {
-        logger.error('Failed to mute account', {message: e})
-        Toast.show(_(msg`There was an issue! ${e.toString()}`))
-      }
-    }
-  }, [track, queueMute, _])
-
-  const onPressUnmuteAccount = React.useCallback(async () => {
-    track('ProfileHeader:UnmuteAccountButtonClicked')
-    try {
-      await queueUnmute()
-      Toast.show(_(msg`Account unmuted`))
-    } catch (e: any) {
-      if (e?.name !== 'AbortError') {
-        logger.error('Failed to unmute account', {message: e})
-        Toast.show(_(msg`There was an issue! ${e.toString()}`))
-      }
-    }
-  }, [track, queueUnmute, _])
-
-  const onPressBlockAccount = React.useCallback(async () => {
-    track('ProfileHeader:BlockAccountButtonClicked')
-    openModal({
-      name: 'confirm',
-      title: _(msg`Block Account`),
-      message: _(
-        msg`Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`,
-      ),
-      onPressConfirm: async () => {
-        try {
-          await queueBlock()
-          Toast.show(_(msg`Account blocked`))
-        } catch (e: any) {
-          if (e?.name !== 'AbortError') {
-            logger.error('Failed to block account', {message: e})
-            Toast.show(_(msg`There was an issue! ${e.toString()}`))
-          }
-        }
-      },
-    })
-  }, [track, queueBlock, openModal, _])
-
-  const onPressUnblockAccount = React.useCallback(async () => {
-    track('ProfileHeader:UnblockAccountButtonClicked')
-    openModal({
-      name: 'confirm',
-      title: _(msg`Unblock Account`),
-      message: _(
-        msg`The account will be able to interact with you after unblocking.`,
-      ),
-      onPressConfirm: async () => {
-        try {
-          await queueUnblock()
-          Toast.show(_(msg`Account unblocked`))
-        } catch (e: any) {
-          if (e?.name !== 'AbortError') {
-            logger.error('Failed to unblock account', {message: e})
-            Toast.show(_(msg`There was an issue! ${e.toString()}`))
-          }
-        }
-      },
-    })
-  }, [track, queueUnblock, openModal, _])
-
-  const onPressReportAccount = React.useCallback(() => {
-    track('ProfileHeader:ReportAccountButtonClicked')
-    openModal({
-      name: 'report',
-      did: profile.did,
-    })
-  }, [track, openModal, profile])
-
-  const isMe = React.useMemo(
-    () => currentAccount?.did === profile.did,
-    [currentAccount, profile],
-  )
-  const dropdownItems: DropdownItem[] = React.useMemo(() => {
-    let items: DropdownItem[] = [
-      {
-        testID: 'profileHeaderDropdownShareBtn',
-        label: isWeb ? _(msg`Copy link to profile`) : _(msg`Share`),
-        onPress: onPressShare,
-        icon: {
-          ios: {
-            name: 'square.and.arrow.up',
-          },
-          android: 'ic_menu_share',
-          web: 'share',
-        },
-      },
-    ]
-    if (hasSession) {
-      items.push({label: 'separator'})
-      items.push({
-        testID: 'profileHeaderDropdownListAddRemoveBtn',
-        label: _(msg`Add to Lists`),
-        onPress: onPressAddRemoveLists,
-        icon: {
-          ios: {
-            name: 'list.bullet',
-          },
-          android: 'ic_menu_add',
-          web: 'list',
-        },
-      })
-      if (!isMe) {
-        if (!profile.viewer?.blocking) {
-          if (!profile.viewer?.mutedByList) {
-            items.push({
-              testID: 'profileHeaderDropdownMuteBtn',
-              label: profile.viewer?.muted
-                ? _(msg`Unmute Account`)
-                : _(msg`Mute Account`),
-              onPress: profile.viewer?.muted
-                ? onPressUnmuteAccount
-                : onPressMuteAccount,
-              icon: {
-                ios: {
-                  name: 'speaker.slash',
-                },
-                android: 'ic_lock_silent_mode',
-                web: 'comment-slash',
-              },
-            })
-          }
-        }
-        if (!profile.viewer?.blockingByList) {
-          items.push({
-            testID: 'profileHeaderDropdownBlockBtn',
-            label: profile.viewer?.blocking
-              ? _(msg`Unblock Account`)
-              : _(msg`Block Account`),
-            onPress: profile.viewer?.blocking
-              ? onPressUnblockAccount
-              : onPressBlockAccount,
-            icon: {
-              ios: {
-                name: 'person.fill.xmark',
-              },
-              android: 'ic_menu_close_clear_cancel',
-              web: 'user-slash',
-            },
-          })
-        }
-        items.push({
-          testID: 'profileHeaderDropdownReportBtn',
-          label: _(msg`Report Account`),
-          onPress: onPressReportAccount,
-          icon: {
-            ios: {
-              name: 'exclamationmark.triangle',
-            },
-            android: 'ic_menu_report_image',
-            web: 'circle-exclamation',
-          },
-        })
-      }
-    }
-    return items
-  }, [
-    isMe,
-    hasSession,
-    profile.viewer?.muted,
-    profile.viewer?.mutedByList,
-    profile.viewer?.blocking,
-    profile.viewer?.blockingByList,
-    onPressShare,
-    onPressUnmuteAccount,
-    onPressMuteAccount,
-    onPressUnblockAccount,
-    onPressBlockAccount,
-    onPressReportAccount,
-    onPressAddRemoveLists,
-    _,
-  ])
-
-  const blockHide =
-    !isMe && (profile.viewer?.blocking || profile.viewer?.blockedBy)
-  const following = formatCount(profile.followsCount || 0)
-  const followers = formatCount(profile.followersCount || 0)
-  const pluralizedFollowers = pluralize(profile.followersCount || 0, 'follower')
-
-  return (
-    <View style={[pal.view]} pointerEvents="box-none">
-      <View pointerEvents="none">
-        {isPlaceholderProfile ? (
-          <LoadingPlaceholder
-            width="100%"
-            height={150}
-            style={{borderRadius: 0}}
-          />
-        ) : (
-          <UserBanner banner={profile.banner} moderation={moderation.avatar} />
-        )}
-      </View>
-      <View style={styles.content} pointerEvents="box-none">
-        <View style={[styles.buttonsLine]} pointerEvents="box-none">
-          {isMe ? (
-            <TouchableOpacity
-              testID="profileHeaderEditProfileButton"
-              onPress={onPressEditProfile}
-              style={[styles.btn, styles.mainBtn, pal.btn]}
-              accessibilityRole="button"
-              accessibilityLabel={_(msg`Edit profile`)}
-              accessibilityHint={_(
-                msg`Opens editor for profile display name, avatar, background image, and description`,
-              )}>
-              <Text type="button" style={pal.text}>
-                <Trans>Edit Profile</Trans>
-              </Text>
-            </TouchableOpacity>
-          ) : profile.viewer?.blocking ? (
-            profile.viewer?.blockingByList ? null : (
-              <TouchableOpacity
-                testID="unblockBtn"
-                onPress={onPressUnblockAccount}
-                style={[styles.btn, styles.mainBtn, pal.btn]}
-                accessibilityRole="button"
-                accessibilityLabel={_(msg`Unblock`)}
-                accessibilityHint="">
-                <Text type="button" style={[pal.text, s.bold]}>
-                  <Trans context="action">Unblock</Trans>
-                </Text>
-              </TouchableOpacity>
-            )
-          ) : !profile.viewer?.blockedBy ? (
-            <>
-              {hasSession && (
-                <TouchableOpacity
-                  testID="suggestedFollowsBtn"
-                  onPress={() => setShowSuggestedFollows(!showSuggestedFollows)}
-                  style={[
-                    styles.btn,
-                    styles.mainBtn,
-                    pal.btn,
-                    {
-                      paddingHorizontal: 10,
-                      backgroundColor: showSuggestedFollows
-                        ? pal.colors.text
-                        : pal.colors.backgroundLight,
-                    },
-                  ]}
-                  accessibilityRole="button"
-                  accessibilityLabel={_(
-                    msg`Show follows similar to ${profile.handle}`,
-                  )}
-                  accessibilityHint={_(
-                    msg`Shows a list of users similar to this user.`,
-                  )}>
-                  <FontAwesomeIcon
-                    icon="user-plus"
-                    style={[
-                      pal.text,
-                      {
-                        color: showSuggestedFollows
-                          ? pal.textInverted.color
-                          : pal.text.color,
-                      },
-                    ]}
-                    size={14}
-                  />
-                </TouchableOpacity>
-              )}
-
-              {profile.viewer?.following ? (
-                <TouchableOpacity
-                  testID="unfollowBtn"
-                  onPress={onPressUnfollow}
-                  style={[styles.btn, styles.mainBtn, pal.btn]}
-                  accessibilityRole="button"
-                  accessibilityLabel={_(msg`Unfollow ${profile.handle}`)}
-                  accessibilityHint={_(
-                    msg`Hides posts from ${profile.handle} in your feed`,
-                  )}>
-                  <FontAwesomeIcon
-                    icon="check"
-                    style={[pal.text, s.mr5]}
-                    size={14}
-                  />
-                  <Text type="button" style={pal.text}>
-                    <Trans>Following</Trans>
-                  </Text>
-                </TouchableOpacity>
-              ) : (
-                <TouchableOpacity
-                  testID="followBtn"
-                  onPress={onPressFollow}
-                  style={[styles.btn, styles.mainBtn, palInverted.view]}
-                  accessibilityRole="button"
-                  accessibilityLabel={_(msg`Follow ${profile.handle}`)}
-                  accessibilityHint={_(
-                    msg`Shows posts from ${profile.handle} in your feed`,
-                  )}>
-                  <FontAwesomeIcon
-                    icon="plus"
-                    style={[palInverted.text, s.mr5]}
-                  />
-                  <Text type="button" style={[palInverted.text, s.bold]}>
-                    <Trans>Follow</Trans>
-                  </Text>
-                </TouchableOpacity>
-              )}
-            </>
-          ) : null}
-          {dropdownItems?.length ? (
-            <NativeDropdown
-              testID="profileHeaderDropdownBtn"
-              items={dropdownItems}
-              accessibilityLabel={_(msg`More options`)}
-              accessibilityHint="">
-              <View style={[styles.btn, styles.secondaryBtn, pal.btn]}>
-                <FontAwesomeIcon icon="ellipsis" size={20} style={[pal.text]} />
-              </View>
-            </NativeDropdown>
-          ) : undefined}
-        </View>
-        <View pointerEvents="none">
-          <Text
-            testID="profileHeaderDisplayName"
-            type="title-2xl"
-            style={[pal.text, styles.title]}>
-            {sanitizeDisplayName(
-              profile.displayName || sanitizeHandle(profile.handle),
-              moderation.profile,
-            )}
-          </Text>
-        </View>
-        <View style={styles.handleLine} pointerEvents="none">
-          {profile.viewer?.followedBy && !blockHide ? (
-            <View style={[styles.pill, pal.btn, s.mr5]}>
-              <Text type="xs" style={[pal.text]}>
-                <Trans>Follows you</Trans>
-              </Text>
-            </View>
-          ) : undefined}
-          <ThemedText
-            type={invalidHandle ? 'xs' : 'md'}
-            fg={invalidHandle ? 'error' : 'light'}
-            border={invalidHandle ? 'error' : undefined}
-            style={[
-              invalidHandle ? styles.invalidHandle : undefined,
-              styles.handle,
-            ]}>
-            {invalidHandle ? _(msg`âš Invalid Handle`) : `@${profile.handle}`}
-          </ThemedText>
-        </View>
-        {!isPlaceholderProfile && !blockHide && (
-          <>
-            <View style={styles.metricsLine} pointerEvents="box-none">
-              <Link
-                testID="profileHeaderFollowersButton"
-                style={[s.flexRow, s.mr10]}
-                href={makeProfileLink(profile, 'followers')}
-                onPressOut={() =>
-                  track(`ProfileHeader:FollowersButtonClicked`, {
-                    handle: profile.handle,
-                  })
-                }
-                asAnchor
-                accessibilityLabel={`${followers} ${pluralizedFollowers}`}
-                accessibilityHint={_(msg`Opens followers list`)}>
-                <Text type="md" style={[s.bold, pal.text]}>
-                  {followers}{' '}
-                </Text>
-                <Text type="md" style={[pal.textLight]}>
-                  {pluralizedFollowers}
-                </Text>
-              </Link>
-              <Link
-                testID="profileHeaderFollowsButton"
-                style={[s.flexRow, s.mr10]}
-                href={makeProfileLink(profile, 'follows')}
-                onPressOut={() =>
-                  track(`ProfileHeader:FollowsButtonClicked`, {
-                    handle: profile.handle,
-                  })
-                }
-                asAnchor
-                accessibilityLabel={_(msg`${following} following`)}
-                accessibilityHint={_(msg`Opens following list`)}>
-                <Trans>
-                  <Text type="md" style={[s.bold, pal.text]}>
-                    {following}{' '}
-                  </Text>
-                  <Text type="md" style={[pal.textLight]}>
-                    following
-                  </Text>
-                </Trans>
-              </Link>
-              <Text type="md" style={[s.bold, pal.text]}>
-                {formatCount(profile.postsCount || 0)}{' '}
-                <Text type="md" style={[pal.textLight]}>
-                  {pluralize(profile.postsCount || 0, 'post')}
-                </Text>
-              </Text>
-            </View>
-            {descriptionRT && !moderation.profile.blur ? (
-              <View pointerEvents="auto">
-                <RichText
-                  testID="profileHeaderDescription"
-                  style={[styles.description, pal.text]}
-                  numberOfLines={15}
-                  richText={descriptionRT}
-                />
-              </View>
-            ) : undefined}
-          </>
-        )}
-        <ProfileHeaderAlerts moderation={moderation} />
-        {isMe && (
-          <LabelInfo details={{did: profile.did}} labels={profile.labels} />
-        )}
-      </View>
-
-      {showSuggestedFollows && (
-        <ProfileHeaderSuggestedFollows
-          actorDid={profile.did}
-          requestDismiss={() => {
-            if (showSuggestedFollows) {
-              setShowSuggestedFollows(false)
-            } else {
-              track('ProfileHeader:SuggestedFollowsOpened')
-              setShowSuggestedFollows(true)
-            }
-          }}
-        />
-      )}
-
-      {!isDesktop && !hideBackButton && (
-        <TouchableWithoutFeedback
-          testID="profileHeaderBackBtn"
-          onPress={onPressBack}
-          hitSlop={BACK_HITSLOP}
-          accessibilityRole="button"
-          accessibilityLabel={_(msg`Back`)}
-          accessibilityHint="">
-          <View style={styles.backBtnWrapper}>
-            <BlurView style={styles.backBtn} blurType="dark">
-              <FontAwesomeIcon size={18} icon="angle-left" style={s.white} />
-            </BlurView>
-          </View>
-        </TouchableWithoutFeedback>
-      )}
-      <TouchableWithoutFeedback
-        testID="profileHeaderAviButton"
-        onPress={onPressAvi}
-        accessibilityRole="image"
-        accessibilityLabel={_(msg`View ${profile.handle}'s avatar`)}
-        accessibilityHint="">
-        <View
-          style={[pal.view, {borderColor: pal.colors.background}, styles.avi]}>
-          <UserAvatar
-            size={80}
-            avatar={profile.avatar}
-            moderation={moderation.avatar}
-          />
-        </View>
-      </TouchableWithoutFeedback>
-    </View>
-  )
-}
-ProfileHeader = memo(ProfileHeader)
-export {ProfileHeader}
-
-const styles = StyleSheet.create({
-  banner: {
-    width: '100%',
-    height: 120,
-  },
-  backBtnWrapper: {
-    position: 'absolute',
-    top: 10,
-    left: 10,
-    width: 30,
-    height: 30,
-    overflow: 'hidden',
-    borderRadius: 15,
-    // @ts-ignore web only
-    cursor: 'pointer',
-  },
-  backBtn: {
-    width: 30,
-    height: 30,
-    borderRadius: 15,
-    alignItems: 'center',
-    justifyContent: 'center',
-  },
-  avi: {
-    position: 'absolute',
-    top: 110,
-    left: 10,
-    width: 84,
-    height: 84,
-    borderRadius: 42,
-    borderWidth: 2,
-  },
-  content: {
-    paddingTop: 8,
-    paddingHorizontal: 14,
-    paddingBottom: 4,
-  },
-
-  buttonsLine: {
-    flexDirection: 'row',
-    marginLeft: 'auto',
-    marginBottom: 12,
-  },
-  primaryBtn: {
-    backgroundColor: colors.blue3,
-    paddingHorizontal: 24,
-    paddingVertical: 6,
-  },
-  mainBtn: {
-    paddingHorizontal: 24,
-  },
-  secondaryBtn: {
-    paddingHorizontal: 14,
-  },
-  btn: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    justifyContent: 'center',
-    paddingVertical: 7,
-    borderRadius: 50,
-    marginLeft: 6,
-  },
-  title: {lineHeight: 38},
-
-  // Word wrapping appears fine on
-  // mobile but overflows on desktop
-  handle: isNative
-    ? {}
-    : {
-        // @ts-ignore web only -prf
-        wordBreak: 'break-all',
-      },
-  invalidHandle: {
-    borderWidth: 1,
-    borderRadius: 4,
-    paddingHorizontal: 4,
-  },
-
-  handleLine: {
-    flexDirection: 'row',
-    marginBottom: 8,
-  },
-
-  metricsLine: {
-    flexDirection: 'row',
-    marginBottom: 8,
-  },
-
-  description: {
-    marginBottom: 8,
-  },
-
-  detailLine: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    marginBottom: 5,
-  },
-
-  pill: {
-    borderRadius: 4,
-    paddingHorizontal: 6,
-    paddingVertical: 2,
-  },
-
-  br40: {borderRadius: 40},
-  br50: {borderRadius: 50},
-})
diff --git a/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx b/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx
index 6edc61fcf..3602cdb9a 100644
--- a/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx
+++ b/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx
@@ -21,7 +21,8 @@ import {useModerationOpts} from '#/state/queries/preferences'
 import {useSuggestedFollowsByActorQuery} from '#/state/queries/suggested-follows'
 import {useProfileShadow} from '#/state/cache/profile-shadow'
 import {useProfileFollowMutationQueue} from '#/state/queries/profile'
-import {Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {Trans, msg} from '@lingui/macro'
 
 const OUTER_PADDING = 10
 const INNER_PADDING = 14
@@ -98,9 +99,11 @@ export function ProfileHeaderSuggestedFollows({
               <SuggestedFollowSkeleton />
             </>
           ) : data ? (
-            data.suggestions.map(profile => (
-              <SuggestedFollow key={profile.did} profile={profile} />
-            ))
+            data.suggestions
+              .filter(s => (s.associated?.labeler ? false : true))
+              .map(profile => (
+                <SuggestedFollow key={profile.did} profile={profile} />
+              ))
           ) : (
             <View />
           )}
@@ -168,9 +171,13 @@ function SuggestedFollow({
 }) {
   const {track} = useAnalytics()
   const pal = usePalette('default')
+  const {_} = useLingui()
   const moderationOpts = useModerationOpts()
   const profile = useProfileShadow(profileUnshadowed)
-  const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(profile)
+  const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(
+    profile,
+    'ProfileHeaderSuggestedFollows',
+  )
 
   const onPressFollow = React.useCallback(async () => {
     try {
@@ -178,20 +185,20 @@ function SuggestedFollow({
       await queueFollow()
     } catch (e: any) {
       if (e?.name !== 'AbortError') {
-        Toast.show('An issue occurred, please try again.')
+        Toast.show(_(msg`An issue occurred, please try again.`))
       }
     }
-  }, [queueFollow, track])
+  }, [queueFollow, track, _])
 
   const onPressUnfollow = React.useCallback(async () => {
     try {
       await queueUnfollow()
     } catch (e: any) {
       if (e?.name !== 'AbortError') {
-        Toast.show('An issue occurred, please try again.')
+        Toast.show(_(msg`An issue occurred, please try again.`))
       }
     }
-  }, [queueUnfollow])
+  }, [queueUnfollow, _])
 
   if (!moderationOpts) {
     return null
@@ -214,7 +221,7 @@ function SuggestedFollow({
         <UserAvatar
           size={60}
           avatar={profile.avatar}
-          moderation={moderation.avatar}
+          moderation={moderation.ui('avatar')}
         />
 
         <View style={{width: '100%', paddingVertical: 12}}>
@@ -224,7 +231,7 @@ function SuggestedFollow({
             numberOfLines={1}>
             {sanitizeDisplayName(
               profile.displayName || sanitizeHandle(profile.handle),
-              moderation.profile,
+              moderation.ui('displayName'),
             )}
           </Text>
           <Text
@@ -236,7 +243,7 @@ function SuggestedFollow({
         </View>
 
         <Button
-          label={following ? 'Unfollow' : 'Follow'}
+          label={following ? _(msg`Unfollow`) : _(msg`Follow`)}
           type="inverted"
           labelStyle={{textAlign: 'center'}}
           onPress={following ? onPressUnfollow : onPressFollow}
diff --git a/src/view/com/profile/ProfileMenu.tsx b/src/view/com/profile/ProfileMenu.tsx
new file mode 100644
index 000000000..cb0b1d97c
--- /dev/null
+++ b/src/view/com/profile/ProfileMenu.tsx
@@ -0,0 +1,380 @@
+import React, {memo} from 'react'
+import {TouchableOpacity} from 'react-native'
+import {AppBskyActorDefs} from '@atproto/api'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {useQueryClient} from '@tanstack/react-query'
+import * as Toast from 'view/com/util/Toast'
+import {EventStopper} from 'view/com/util/EventStopper'
+import {useSession} from 'state/session'
+import * as Menu from '#/components/Menu'
+import {useTheme} from '#/alf'
+import {usePalette} from 'lib/hooks/usePalette'
+import {HITSLOP_10} from 'lib/constants'
+import {shareUrl} from 'lib/sharing'
+import {toShareUrl} from 'lib/strings/url-helpers'
+import {makeProfileLink} from 'lib/routes/links'
+import {useAnalytics} from 'lib/analytics/analytics'
+import {useModalControls} from 'state/modals'
+import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog'
+import {
+  RQKEY as profileQueryKey,
+  useProfileBlockMutationQueue,
+  useProfileFollowMutationQueue,
+  useProfileMuteMutationQueue,
+} from 'state/queries/profile'
+import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox'
+import {ListSparkle_Stroke2_Corner0_Rounded as List} from '#/components/icons/ListSparkle'
+import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute'
+import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker'
+import {Flag_Stroke2_Corner0_Rounded as Flag} from '#/components/icons/Flag'
+import {PersonCheck_Stroke2_Corner0_Rounded as PersonCheck} from '#/components/icons/PersonCheck'
+import {PersonX_Stroke2_Corner0_Rounded as PersonX} from '#/components/icons/PersonX'
+import {PeopleRemove2_Stroke2_Corner0_Rounded as UserMinus} from '#/components/icons/PeopleRemove2'
+import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
+import {logger} from '#/logger'
+import {Shadow} from 'state/cache/types'
+import * as Prompt from '#/components/Prompt'
+
+let ProfileMenu = ({
+  profile,
+}: {
+  profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>
+}): React.ReactNode => {
+  const {_} = useLingui()
+  const {currentAccount, hasSession} = useSession()
+  const t = useTheme()
+  // TODO ALF this
+  const pal = usePalette('default')
+  const {track} = useAnalytics()
+  const {openModal} = useModalControls()
+  const reportDialogControl = useReportDialogControl()
+  const queryClient = useQueryClient()
+  const isSelf = currentAccount?.did === profile.did
+  const isFollowing = profile.viewer?.following
+  const isBlocked = profile.viewer?.blocking || profile.viewer?.blockedBy
+  const isFollowingBlockedAccount = isFollowing && isBlocked
+  const isLabelerAndNotBlocked = !!profile.associated?.labeler && !isBlocked
+
+  const [queueMute, queueUnmute] = useProfileMuteMutationQueue(profile)
+  const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile)
+  const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(
+    profile,
+    'ProfileMenu',
+  )
+
+  const blockPromptControl = Prompt.usePromptControl()
+  const loggedOutWarningPromptControl = Prompt.usePromptControl()
+
+  const showLoggedOutWarning = React.useMemo(() => {
+    return !!profile.labels?.find(label => label.val === '!no-unauthenticated')
+  }, [profile.labels])
+
+  const invalidateProfileQuery = React.useCallback(() => {
+    queryClient.invalidateQueries({
+      queryKey: profileQueryKey(profile.did),
+    })
+  }, [queryClient, profile.did])
+
+  const onPressShare = React.useCallback(() => {
+    track('ProfileHeader:ShareButtonClicked')
+    shareUrl(toShareUrl(makeProfileLink(profile)))
+  }, [track, profile])
+
+  const onPressAddRemoveLists = React.useCallback(() => {
+    track('ProfileHeader:AddToListsButtonClicked')
+    openModal({
+      name: 'user-add-remove-lists',
+      subject: profile.did,
+      handle: profile.handle,
+      displayName: profile.displayName || profile.handle,
+      onAdd: invalidateProfileQuery,
+      onRemove: invalidateProfileQuery,
+    })
+  }, [track, profile, openModal, invalidateProfileQuery])
+
+  const onPressMuteAccount = React.useCallback(async () => {
+    if (profile.viewer?.muted) {
+      track('ProfileHeader:UnmuteAccountButtonClicked')
+      try {
+        await queueUnmute()
+        Toast.show(_(msg`Account unmuted`))
+      } catch (e: any) {
+        if (e?.name !== 'AbortError') {
+          logger.error('Failed to unmute account', {message: e})
+          Toast.show(_(msg`There was an issue! ${e.toString()}`))
+        }
+      }
+    } else {
+      track('ProfileHeader:MuteAccountButtonClicked')
+      try {
+        await queueMute()
+        Toast.show(_(msg`Account muted`))
+      } catch (e: any) {
+        if (e?.name !== 'AbortError') {
+          logger.error('Failed to mute account', {message: e})
+          Toast.show(_(msg`There was an issue! ${e.toString()}`))
+        }
+      }
+    }
+  }, [profile.viewer?.muted, track, queueUnmute, _, queueMute])
+
+  const blockAccount = React.useCallback(async () => {
+    if (profile.viewer?.blocking) {
+      track('ProfileHeader:UnblockAccountButtonClicked')
+      try {
+        await queueUnblock()
+        Toast.show(_(msg`Account unblocked`))
+      } catch (e: any) {
+        if (e?.name !== 'AbortError') {
+          logger.error('Failed to unblock account', {message: e})
+          Toast.show(_(msg`There was an issue! ${e.toString()}`))
+        }
+      }
+    } else {
+      track('ProfileHeader:BlockAccountButtonClicked')
+      try {
+        await queueBlock()
+        Toast.show(_(msg`Account blocked`))
+      } catch (e: any) {
+        if (e?.name !== 'AbortError') {
+          logger.error('Failed to block account', {message: e})
+          Toast.show(_(msg`There was an issue! ${e.toString()}`))
+        }
+      }
+    }
+  }, [profile.viewer?.blocking, track, _, queueUnblock, queueBlock])
+
+  const onPressFollowAccount = React.useCallback(async () => {
+    track('ProfileHeader:FollowButtonClicked')
+    try {
+      await queueFollow()
+      Toast.show(_(msg`Account followed`))
+    } catch (e: any) {
+      if (e?.name !== 'AbortError') {
+        logger.error('Failed to follow account', {message: e})
+        Toast.show(_(msg`There was an issue! ${e.toString()}`))
+      }
+    }
+  }, [_, queueFollow, track])
+
+  const onPressUnfollowAccount = React.useCallback(async () => {
+    track('ProfileHeader:UnfollowButtonClicked')
+    try {
+      await queueUnfollow()
+      Toast.show(_(msg`Account unfollowed`))
+    } catch (e: any) {
+      if (e?.name !== 'AbortError') {
+        logger.error('Failed to unfollow account', {message: e})
+        Toast.show(_(msg`There was an issue! ${e.toString()}`))
+      }
+    }
+  }, [_, queueUnfollow, track])
+
+  const onPressReportAccount = React.useCallback(() => {
+    track('ProfileHeader:ReportAccountButtonClicked')
+    reportDialogControl.open()
+  }, [track, reportDialogControl])
+
+  return (
+    <EventStopper onKeyDown={false}>
+      <Menu.Root>
+        <Menu.Trigger label={_(`More options`)}>
+          {({props}) => {
+            return (
+              <TouchableOpacity
+                {...props}
+                hitSlop={HITSLOP_10}
+                testID="profileHeaderDropdownBtn"
+                style={[
+                  {
+                    flexDirection: 'row',
+                    alignItems: 'center',
+                    justifyContent: 'center',
+                    paddingVertical: 10,
+                    borderRadius: 50,
+                    paddingHorizontal: 16,
+                  },
+                  pal.btn,
+                ]}>
+                <FontAwesomeIcon
+                  icon="ellipsis"
+                  size={20}
+                  style={t.atoms.text}
+                />
+              </TouchableOpacity>
+            )
+          }}
+        </Menu.Trigger>
+
+        <Menu.Outer style={{minWidth: 170}}>
+          <Menu.Group>
+            <Menu.Item
+              testID="profileHeaderDropdownShareBtn"
+              label={_(msg`Share`)}
+              onPress={() => {
+                if (showLoggedOutWarning) {
+                  loggedOutWarningPromptControl.open()
+                } else {
+                  onPressShare()
+                }
+              }}>
+              <Menu.ItemText>
+                <Trans>Share</Trans>
+              </Menu.ItemText>
+              <Menu.ItemIcon icon={Share} />
+            </Menu.Item>
+          </Menu.Group>
+
+          {hasSession && (
+            <>
+              <Menu.Divider />
+              <Menu.Group>
+                {!isSelf && (
+                  <>
+                    {(isLabelerAndNotBlocked || isFollowingBlockedAccount) && (
+                      <Menu.Item
+                        testID="profileHeaderDropdownFollowBtn"
+                        label={
+                          isFollowing
+                            ? _(msg`Unfollow Account`)
+                            : _(msg`Follow Account`)
+                        }
+                        onPress={
+                          isFollowing
+                            ? onPressUnfollowAccount
+                            : onPressFollowAccount
+                        }>
+                        <Menu.ItemText>
+                          {isFollowing ? (
+                            <Trans>Unfollow Account</Trans>
+                          ) : (
+                            <Trans>Follow Account</Trans>
+                          )}
+                        </Menu.ItemText>
+                        <Menu.ItemIcon icon={isFollowing ? UserMinus : Plus} />
+                      </Menu.Item>
+                    )}
+                  </>
+                )}
+                <Menu.Item
+                  testID="profileHeaderDropdownListAddRemoveBtn"
+                  label={_(msg`Add to Lists`)}
+                  onPress={onPressAddRemoveLists}>
+                  <Menu.ItemText>
+                    <Trans>Add to Lists</Trans>
+                  </Menu.ItemText>
+                  <Menu.ItemIcon icon={List} />
+                </Menu.Item>
+                {!isSelf && (
+                  <>
+                    {!profile.viewer?.blocking &&
+                      !profile.viewer?.mutedByList && (
+                        <Menu.Item
+                          testID="profileHeaderDropdownMuteBtn"
+                          label={
+                            profile.viewer?.muted
+                              ? _(msg`Unmute Account`)
+                              : _(msg`Mute Account`)
+                          }
+                          onPress={onPressMuteAccount}>
+                          <Menu.ItemText>
+                            {profile.viewer?.muted ? (
+                              <Trans>Unmute Account</Trans>
+                            ) : (
+                              <Trans>Mute Account</Trans>
+                            )}
+                          </Menu.ItemText>
+                          <Menu.ItemIcon
+                            icon={profile.viewer?.muted ? Unmute : Mute}
+                          />
+                        </Menu.Item>
+                      )}
+                    {!profile.viewer?.blockingByList && (
+                      <Menu.Item
+                        testID="profileHeaderDropdownBlockBtn"
+                        label={
+                          profile.viewer
+                            ? _(msg`Unblock Account`)
+                            : _(msg`Block Account`)
+                        }
+                        onPress={() => blockPromptControl.open()}>
+                        <Menu.ItemText>
+                          {profile.viewer?.blocking ? (
+                            <Trans>Unblock Account</Trans>
+                          ) : (
+                            <Trans>Block Account</Trans>
+                          )}
+                        </Menu.ItemText>
+                        <Menu.ItemIcon
+                          icon={
+                            profile.viewer?.blocking ? PersonCheck : PersonX
+                          }
+                        />
+                      </Menu.Item>
+                    )}
+                    <Menu.Item
+                      testID="profileHeaderDropdownReportBtn"
+                      label={_(msg`Report Account`)}
+                      onPress={onPressReportAccount}>
+                      <Menu.ItemText>
+                        <Trans>Report Account</Trans>
+                      </Menu.ItemText>
+                      <Menu.ItemIcon icon={Flag} />
+                    </Menu.Item>
+                  </>
+                )}
+              </Menu.Group>
+            </>
+          )}
+        </Menu.Outer>
+      </Menu.Root>
+
+      <ReportDialog
+        control={reportDialogControl}
+        params={{type: 'account', did: profile.did}}
+      />
+
+      <Prompt.Basic
+        control={blockPromptControl}
+        title={
+          profile.viewer?.blocking
+            ? _(msg`Unblock Account?`)
+            : _(msg`Block Account?`)
+        }
+        description={
+          profile.viewer?.blocking
+            ? _(
+                msg`The account will be able to interact with you after unblocking.`,
+              )
+            : profile.associated?.labeler
+            ? _(
+                msg`Blocking will not prevent labels from being applied on your account, but it will stop this account from replying in your threads or interacting with you.`,
+              )
+            : _(
+                msg`Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`,
+              )
+        }
+        onConfirm={blockAccount}
+        confirmButtonCta={
+          profile.viewer?.blocking ? _(msg`Unblock`) : _(msg`Block`)
+        }
+        confirmButtonColor={profile.viewer?.blocking ? undefined : 'negative'}
+      />
+
+      <Prompt.Basic
+        control={loggedOutWarningPromptControl}
+        title={_(msg`Note about sharing`)}
+        description={_(
+          msg`This profile is only visible to logged-in users. It won't be visible to people who aren't logged in.`,
+        )}
+        onConfirm={onPressShare}
+        confirmButtonCta={_(msg`Share anyway`)}
+      />
+    </EventStopper>
+  )
+}
+
+ProfileMenu = memo(ProfileMenu)
+export {ProfileMenu}
diff --git a/src/view/com/testing/TestCtrls.e2e.tsx b/src/view/com/testing/TestCtrls.e2e.tsx
index e1e899488..1eb99c4f5 100644
--- a/src/view/com/testing/TestCtrls.e2e.tsx
+++ b/src/view/com/testing/TestCtrls.e2e.tsx
@@ -22,18 +22,24 @@ export function TestCtrls() {
   const {mutate: setFeedViewPref} = useSetFeedViewPreferencesMutation()
   const {setShowLoggedOut} = useLoggedOutViewControls()
   const onPressSignInAlice = async () => {
-    await login({
-      service: 'http://localhost:3000',
-      identifier: 'alice.test',
-      password: 'hunter2',
-    })
+    await login(
+      {
+        service: 'http://localhost:3000',
+        identifier: 'alice.test',
+        password: 'hunter2',
+      },
+      'LoginForm',
+    )
   }
   const onPressSignInBob = async () => {
-    await login({
-      service: 'http://localhost:3000',
-      identifier: 'bob.test',
-      password: 'hunter2',
-    })
+    await login(
+      {
+        service: 'http://localhost:3000',
+        identifier: 'bob.test',
+        password: 'hunter2',
+      },
+      'LoginForm',
+    )
   }
   return (
     <View style={{position: 'absolute', top: 100, right: 0, zIndex: 100}}>
@@ -51,7 +57,7 @@ export function TestCtrls() {
       />
       <Pressable
         testID="e2eSignOut"
-        onPress={() => logout()}
+        onPress={() => logout('Settings')}
         accessibilityRole="button"
         style={BTN}
       />
diff --git a/src/view/com/util/BottomSheetCustomBackdrop.tsx b/src/view/com/util/BottomSheetCustomBackdrop.tsx
index ed5a2f165..0d15c5e55 100644
--- a/src/view/com/util/BottomSheetCustomBackdrop.tsx
+++ b/src/view/com/util/BottomSheetCustomBackdrop.tsx
@@ -1,17 +1,20 @@
 import React, {useMemo} from 'react'
 import {TouchableWithoutFeedback} from 'react-native'
-import {BottomSheetBackdropProps} from '@gorhom/bottom-sheet'
 import Animated, {
   Extrapolate,
   interpolate,
   useAnimatedStyle,
 } from 'react-native-reanimated'
-import {t} from '@lingui/macro'
+import {BottomSheetBackdropProps} from '@discord/bottom-sheet/src'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
 
 export function createCustomBackdrop(
   onClose?: (() => void) | undefined,
 ): React.FC<BottomSheetBackdropProps> {
   const CustomBackdrop = ({animatedIndex, style}: BottomSheetBackdropProps) => {
+    const {_} = useLingui()
+
     // animated variables
     const opacity = useAnimatedStyle(() => ({
       opacity: interpolate(
@@ -30,7 +33,7 @@ export function createCustomBackdrop(
     return (
       <TouchableWithoutFeedback
         onPress={onClose}
-        accessibilityLabel={t`Close bottom drawer`}
+        accessibilityLabel={_(msg`Close bottom drawer`)}
         accessibilityHint=""
         onAccessibilityEscape={() => {
           if (onClose !== undefined) {
diff --git a/src/view/com/util/ErrorBoundary.tsx b/src/view/com/util/ErrorBoundary.tsx
index 5ec1d0014..22fdd606e 100644
--- a/src/view/com/util/ErrorBoundary.tsx
+++ b/src/view/com/util/ErrorBoundary.tsx
@@ -1,8 +1,9 @@
 import React, {Component, ErrorInfo, ReactNode} from 'react'
 import {ErrorScreen} from './error/ErrorScreen'
 import {CenteredView} from './Views'
-import {t} from '@lingui/macro'
+import {msg} from '@lingui/macro'
 import {logger} from '#/logger'
+import {useLingui} from '@lingui/react'
 
 interface Props {
   children?: ReactNode
@@ -31,11 +32,7 @@ export class ErrorBoundary extends Component<Props, State> {
     if (this.state.hasError) {
       return (
         <CenteredView style={{height: '100%', flex: 1}}>
-          <ErrorScreen
-            title={t`Oh no!`}
-            message={t`There was an unexpected issue in the application. Please let us know if this happened to you!`}
-            details={this.state.error.toString()}
-          />
+          <TranslatedErrorScreen details={this.state.error.toString()} />
         </CenteredView>
       )
     }
@@ -43,3 +40,17 @@ export class ErrorBoundary extends Component<Props, State> {
     return this.props.children
   }
 }
+
+function TranslatedErrorScreen({details}: {details?: string}) {
+  const {_} = useLingui()
+
+  return (
+    <ErrorScreen
+      title={_(msg`Oh no!`)}
+      message={_(
+        msg`There was an unexpected issue in the application. Please let us know if this happened to you!`,
+      )}
+      details={details}
+    />
+  )
+}
diff --git a/src/view/com/util/EventStopper.tsx b/src/view/com/util/EventStopper.tsx
index 1e672e945..8f5f5cf54 100644
--- a/src/view/com/util/EventStopper.tsx
+++ b/src/view/com/util/EventStopper.tsx
@@ -1,11 +1,21 @@
 import React from 'react'
-import {View} from 'react-native'
+import {View, ViewStyle} from 'react-native'
 
 /**
  * This utility function captures events and stops
  * them from propagating upwards.
  */
-export function EventStopper({children}: React.PropsWithChildren<{}>) {
+export function EventStopper({
+  children,
+  style,
+  onKeyDown = true,
+}: React.PropsWithChildren<{
+  style?: ViewStyle | ViewStyle[]
+  /**
+   * Default `true`. Set to `false` to allow onKeyDown to propagate
+   */
+  onKeyDown?: boolean
+}>) {
   const stop = (e: any) => {
     e.stopPropagation()
   }
@@ -15,7 +25,8 @@ export function EventStopper({children}: React.PropsWithChildren<{}>) {
       onTouchEnd={stop}
       // @ts-ignore web only -prf
       onClick={stop}
-      onKeyDown={stop}>
+      onKeyDown={onKeyDown ? stop : undefined}
+      style={style}>
       {children}
     </View>
   )
diff --git a/src/view/com/util/Link.tsx b/src/view/com/util/Link.tsx
index d52d3c0e6..b6c512b09 100644
--- a/src/view/com/util/Link.tsx
+++ b/src/view/com/util/Link.tsx
@@ -8,17 +8,11 @@ import {
   View,
   ViewStyle,
   Pressable,
-  TouchableWithoutFeedback,
   TouchableOpacity,
 } from 'react-native'
-import {
-  useLinkProps,
-  useNavigation,
-  StackActions,
-} from '@react-navigation/native'
+import {useLinkProps, StackActions} from '@react-navigation/native'
 import {Text} from './text/Text'
 import {TypographyVariant} from 'lib/ThemeContext'
-import {NavigationProp} from 'lib/routes/types'
 import {router} from '../../../routes'
 import {
   convertBskyAppUrlIfNeeded,
@@ -28,10 +22,14 @@ import {
 import {isAndroid, isWeb} from 'platform/detection'
 import {sanitizeUrl} from '@braintree/sanitize-url'
 import {PressableWithHover} from './PressableWithHover'
-import FixedTouchableHighlight from '../pager/FixedTouchableHighlight'
 import {useModalControls} from '#/state/modals'
 import {useOpenLink} from '#/state/preferences/in-app-browser'
 import {WebAuxClickWrapper} from 'view/com/util/WebAuxClickWrapper'
+import {
+  DebouncedNavigationProp,
+  useNavigationDeduped,
+} from 'lib/hooks/useNavigationDeduped'
+import {useTheme} from '#/alf'
 
 type Event =
   | React.MouseEvent<HTMLAnchorElement, MouseEvent>
@@ -49,6 +47,7 @@ interface Props extends ComponentProps<typeof TouchableOpacity> {
   anchorNoUnderline?: boolean
   navigationAction?: 'push' | 'replace' | 'navigate'
   onPointerEnter?: () => void
+  onBeforePress?: () => void
 }
 
 export const Link = memo(function Link({
@@ -62,15 +61,18 @@ export const Link = memo(function Link({
   accessible,
   anchorNoUnderline,
   navigationAction,
+  onBeforePress,
   ...props
 }: Props) {
+  const t = useTheme()
   const {closeModal} = useModalControls()
-  const navigation = useNavigation<NavigationProp>()
+  const navigation = useNavigationDeduped()
   const anchorHref = asAnchor ? sanitizeUrl(href) : undefined
   const openLink = useOpenLink()
 
   const onPress = React.useCallback(
     (e?: Event) => {
+      onBeforePress?.()
       if (typeof href === 'string') {
         return onPressInner(
           closeModal,
@@ -82,41 +84,27 @@ export const Link = memo(function Link({
         )
       }
     },
-    [closeModal, navigation, navigationAction, href, openLink],
+    [closeModal, navigation, navigationAction, href, openLink, onBeforePress],
   )
 
   if (noFeedback) {
-    if (isAndroid) {
-      // workaround for Android not working well with left/right swipe gestures and TouchableWithoutFeedback
-      // https://github.com/callstack/react-native-pager-view/issues/424
-      return (
-        <FixedTouchableHighlight
-          testID={testID}
-          onPress={onPress}
-          // @ts-ignore web only -prf
-          href={asAnchor ? sanitizeUrl(href) : undefined}
-          accessible={accessible}
-          accessibilityRole="link"
-          {...props}>
-          <View style={style}>
-            {children ? children : <Text>{title || 'link'}</Text>}
-          </View>
-        </FixedTouchableHighlight>
-      )
-    }
     return (
       <WebAuxClickWrapper>
-        <TouchableWithoutFeedback
+        <Pressable
           testID={testID}
           onPress={onPress}
           accessible={accessible}
           accessibilityRole="link"
-          {...props}>
+          {...props}
+          android_ripple={{
+            color: t.atoms.bg_contrast_25.backgroundColor,
+          }}
+          unstable_pressDelay={isAndroid ? 90 : undefined}>
           {/* @ts-ignore web only -prf */}
           <View style={style} href={anchorHref}>
             {children ? children : <Text>{title || 'link'}</Text>}
           </View>
-        </TouchableWithoutFeedback>
+        </Pressable>
       </WebAuxClickWrapper>
     )
   }
@@ -159,7 +147,7 @@ export const TextLink = memo(function TextLink({
   dataSet,
   title,
   onPress,
-  warnOnMismatchingLabel,
+  disableMismatchWarning,
   navigationAction,
   ...orgProps
 }: {
@@ -172,22 +160,22 @@ export const TextLink = memo(function TextLink({
   lineHeight?: number
   dataSet?: any
   title?: string
-  warnOnMismatchingLabel?: boolean
+  disableMismatchWarning?: boolean
   navigationAction?: 'push' | 'replace' | 'navigate'
 } & TextProps) {
   const {...props} = useLinkProps({to: sanitizeUrl(href)})
-  const navigation = useNavigation<NavigationProp>()
+  const navigation = useNavigationDeduped()
   const {openModal, closeModal} = useModalControls()
   const openLink = useOpenLink()
 
-  if (warnOnMismatchingLabel && typeof text !== 'string') {
+  if (!disableMismatchWarning && typeof text !== 'string') {
     console.error('Unable to detect mismatching label')
   }
 
   props.onPress = React.useCallback(
     (e?: Event) => {
       const requiresWarning =
-        warnOnMismatchingLabel &&
+        !disableMismatchWarning &&
         linkRequiresWarning(href, typeof text === 'string' ? text : '')
       if (requiresWarning) {
         e?.preventDefault?.()
@@ -227,7 +215,7 @@ export const TextLink = memo(function TextLink({
       navigation,
       href,
       text,
-      warnOnMismatchingLabel,
+      disableMismatchWarning,
       navigationAction,
       openLink,
     ],
@@ -277,6 +265,7 @@ interface TextLinkOnWebOnlyProps extends TextProps {
   accessibilityHint?: string
   title?: string
   navigationAction?: 'push' | 'replace' | 'navigate'
+  disableMismatchWarning?: boolean
   onPointerEnter?: () => void
 }
 export const TextLinkOnWebOnly = memo(function DesktopWebTextLink({
@@ -288,6 +277,7 @@ export const TextLinkOnWebOnly = memo(function DesktopWebTextLink({
   numberOfLines,
   lineHeight,
   navigationAction,
+  disableMismatchWarning,
   ...props
 }: TextLinkOnWebOnlyProps) {
   if (isWeb) {
@@ -302,6 +292,7 @@ export const TextLinkOnWebOnly = memo(function DesktopWebTextLink({
         lineHeight={lineHeight}
         title={props.title}
         navigationAction={navigationAction}
+        disableMismatchWarning={disableMismatchWarning}
         {...props}
       />
     )
@@ -335,7 +326,7 @@ const EXEMPT_PATHS = ['/robots.txt', '/security.txt', '/.well-known/']
 // -prf
 function onPressInner(
   closeModal = () => {},
-  navigation: NavigationProp,
+  navigation: DebouncedNavigationProp,
   href: string,
   navigationAction: 'push' | 'replace' | 'navigate' = 'push',
   openLink: (href: string) => void,
diff --git a/src/view/com/util/List.web.tsx b/src/view/com/util/List.web.tsx
index 29bad2db8..936bac198 100644
--- a/src/view/com/util/List.web.tsx
+++ b/src/view/com/util/List.web.tsx
@@ -172,7 +172,7 @@ function ListImpl<ItemT>(
       <View
         ref={containerRef}
         style={[
-          styles.contentContainer,
+          !isMobile && styles.sideBorders,
           contentContainerStyle,
           desktopFixedHeight ? styles.minHeightViewport : null,
           pal.border,
@@ -304,7 +304,7 @@ export const List = memo(React.forwardRef(ListImpl)) as <ItemT>(
 const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent)
 
 const styles = StyleSheet.create({
-  contentContainer: {
+  sideBorders: {
     borderLeftWidth: 1,
     borderRightWidth: 1,
   },
diff --git a/src/view/com/util/MainScrollProvider.tsx b/src/view/com/util/MainScrollProvider.tsx
index 2c90e33ff..01b8a954d 100644
--- a/src/view/com/util/MainScrollProvider.tsx
+++ b/src/view/com/util/MainScrollProvider.tsx
@@ -20,12 +20,14 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
   const setMode = useSetMinimalShellMode()
   const startDragOffset = useSharedValue<number | null>(null)
   const startMode = useSharedValue<number | null>(null)
+  const didJustRestoreScroll = useSharedValue<boolean>(false)
 
   useEffect(() => {
     if (isWeb) {
       return listenToForcedWindowScroll(() => {
         startDragOffset.value = null
         startMode.value = null
+        didJustRestoreScroll.value = true
       })
     }
   })
@@ -86,6 +88,11 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
           mode.value = newValue
         }
       } else {
+        if (didJustRestoreScroll.value) {
+          didJustRestoreScroll.value = false
+          // Don't hide/show navbar based on scroll restoratoin.
+          return
+        }
         // On the web, we don't try to follow the drag because we don't know when it ends.
         // Instead, show/hide immediately based on whether we're scrolling up or down.
         const dy = e.contentOffset.y - (startDragOffset.value ?? 0)
@@ -98,7 +105,14 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
         }
       }
     },
-    [headerHeight, mode, setMode, startDragOffset, startMode],
+    [
+      headerHeight,
+      mode,
+      setMode,
+      startDragOffset,
+      startMode,
+      didJustRestoreScroll,
+    ],
   )
 
   return (
diff --git a/src/view/com/util/PostMeta.tsx b/src/view/com/util/PostMeta.tsx
index 3795dcf13..529fc54e0 100644
--- a/src/view/com/util/PostMeta.tsx
+++ b/src/view/com/util/PostMeta.tsx
@@ -11,16 +11,12 @@ import {sanitizeHandle} from 'lib/strings/handles'
 import {isAndroid, isWeb} from 'platform/detection'
 import {TimeElapsed} from './TimeElapsed'
 import {makeProfileLink} from 'lib/routes/links'
-import {ModerationUI} from '@atproto/api'
+import {AppBskyActorDefs, ModerationDecision, ModerationUI} from '@atproto/api'
 import {usePrefetchProfileQuery} from '#/state/queries/profile'
 
 interface PostMetaOpts {
-  author: {
-    avatar?: string
-    did: string
-    handle: string
-    displayName?: string | undefined
-  }
+  author: AppBskyActorDefs.ProfileViewBasic
+  moderation: ModerationDecision | undefined
   authorHasWarning: boolean
   postHref: string
   timestamp: string
@@ -46,6 +42,7 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
             avatar={opts.author.avatar}
             size={opts.avatarSize || 16}
             moderation={opts.avatarModeration}
+            type={opts.author.associated?.labeler ? 'labeler' : 'user'}
           />
         </View>
       )}
@@ -55,9 +52,14 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
           style={[pal.text, opts.displayNameStyle]}
           numberOfLines={1}
           lineHeight={1.2}
+          disableMismatchWarning
           text={
             <>
-              {sanitizeDisplayName(displayName)}&nbsp;
+              {sanitizeDisplayName(
+                displayName,
+                opts.moderation?.ui('displayName'),
+              )}
+              &nbsp;
               <Text
                 type="md"
                 numberOfLines={1}
diff --git a/src/view/com/util/PostSandboxWarning.tsx b/src/view/com/util/PostSandboxWarning.tsx
deleted file mode 100644
index b2375c703..000000000
--- a/src/view/com/util/PostSandboxWarning.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import React from 'react'
-import {StyleSheet, View} from 'react-native'
-import {Text} from './text/Text'
-import {usePalette} from 'lib/hooks/usePalette'
-import {useSession} from '#/state/session'
-
-export function PostSandboxWarning() {
-  const {isSandbox} = useSession()
-  const pal = usePalette('default')
-  if (isSandbox) {
-    return (
-      <View style={styles.container}>
-        <Text
-          type="title-2xl"
-          style={[pal.text, styles.text]}
-          accessible={false}>
-          SANDBOX
-        </Text>
-      </View>
-    )
-  }
-  return null
-}
-
-const styles = StyleSheet.create({
-  container: {
-    position: 'absolute',
-    top: 6,
-    right: 10,
-  },
-  text: {
-    fontWeight: 'bold',
-    opacity: 0.07,
-  },
-})
diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx
index f673db1ee..4beedbd5b 100644
--- a/src/view/com/util/UserAvatar.tsx
+++ b/src/view/com/util/UserAvatar.tsx
@@ -1,9 +1,13 @@
 import React, {memo, useMemo} from 'react'
-import {Image, StyleSheet, View} from 'react-native'
+import {Image, StyleSheet, TouchableOpacity, View} from 'react-native'
 import Svg, {Circle, Rect, Path} from 'react-native-svg'
+import {Image as RNImage} from 'react-native-image-crop-picker'
+import {useLingui} from '@lingui/react'
+import {msg, Trans} from '@lingui/macro'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {HighPriorityImage} from 'view/com/util/images/Image'
 import {ModerationUI} from '@atproto/api'
+
+import {HighPriorityImage} from 'view/com/util/images/Image'
 import {openCamera, openCropper, openPicker} from '../../../lib/media/picker'
 import {
   usePhotoLibraryPermission,
@@ -11,14 +15,18 @@ import {
 } from 'lib/hooks/usePermissions'
 import {colors} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
-import {isWeb, isAndroid} from 'platform/detection'
-import {Image as RNImage} from 'react-native-image-crop-picker'
+import {isWeb, isAndroid, isNative} from 'platform/detection'
 import {UserPreviewLink} from './UserPreviewLink'
-import {DropdownItem, NativeDropdown} from './forms/NativeDropdown'
-import {useLingui} from '@lingui/react'
-import {msg} from '@lingui/macro'
+import * as Menu from '#/components/Menu'
+import {
+  Camera_Stroke2_Corner0_Rounded as Camera,
+  Camera_Filled_Stroke2_Corner0_Rounded as CameraFilled,
+} from '#/components/icons/Camera'
+import {StreamingLive_Stroke2_Corner0_Rounded as Library} from '#/components/icons/StreamingLive'
+import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
+import {useTheme, tokens} from '#/alf'
 
-export type UserAvatarType = 'user' | 'algo' | 'list'
+export type UserAvatarType = 'user' | 'algo' | 'list' | 'labeler'
 
 interface BaseUserAvatarProps {
   type?: UserAvatarType
@@ -93,6 +101,33 @@ let DefaultAvatar = ({
       </Svg>
     )
   }
+  if (type === 'labeler') {
+    return (
+      <Svg
+        testID="userAvatarFallback"
+        width={size}
+        height={size}
+        viewBox="0 0 32 32"
+        fill="none"
+        stroke="none">
+        <Rect
+          x="0"
+          y="0"
+          width="32"
+          height="32"
+          rx="3"
+          fill={tokens.color.temp_purple}
+        />
+        <Path
+          d="M24 9.75L16 7L8 9.75V15.9123C8 20.8848 12 23 16 25.1579C20 23 24 20.8848 24 15.9123V9.75Z"
+          stroke="white"
+          strokeWidth="2"
+          strokeLinecap="square"
+          strokeLinejoin="round"
+        />
+      </Svg>
+    )
+  }
   return (
     <Svg
       testID="userAvatarFallback"
@@ -126,7 +161,7 @@ let UserAvatar = ({
   const backgroundColor = pal.colors.backgroundLight
 
   const aviStyle = useMemo(() => {
-    if (type === 'algo' || type === 'list') {
+    if (type === 'algo' || type === 'list' || type === 'labeler') {
       return {
         width: size,
         height: size,
@@ -196,6 +231,7 @@ let EditableUserAvatar = ({
   avatar,
   onSelectNewAvatar,
 }: EditableUserAvatarProps): React.ReactNode => {
+  const t = useTheme()
   const pal = usePalette('default')
   const {_} = useLingui()
   const {requestCameraAccessIfNeeded} = useCameraPermission()
@@ -216,118 +252,118 @@ let EditableUserAvatar = ({
     }
   }, [type, size])
 
-  const dropdownItems = useMemo(
-    () =>
-      [
-        !isWeb && {
-          testID: 'changeAvatarCameraBtn',
-          label: _(msg`Camera`),
-          icon: {
-            ios: {
-              name: 'camera',
-            },
-            android: 'ic_menu_camera',
-            web: 'camera',
-          },
-          onPress: async () => {
-            if (!(await requestCameraAccessIfNeeded())) {
-              return
-            }
+  const onOpenCamera = React.useCallback(async () => {
+    if (!(await requestCameraAccessIfNeeded())) {
+      return
+    }
+
+    onSelectNewAvatar(
+      await openCamera({
+        width: 1000,
+        height: 1000,
+        cropperCircleOverlay: true,
+      }),
+    )
+  }, [onSelectNewAvatar, requestCameraAccessIfNeeded])
+
+  const onOpenLibrary = React.useCallback(async () => {
+    if (!(await requestPhotoAccessIfNeeded())) {
+      return
+    }
 
-            onSelectNewAvatar(
-              await openCamera({
-                width: 1000,
-                height: 1000,
-                cropperCircleOverlay: true,
-              }),
-            )
-          },
-        },
-        {
-          testID: 'changeAvatarLibraryBtn',
-          label: _(msg`Library`),
-          icon: {
-            ios: {
-              name: 'photo.on.rectangle.angled',
-            },
-            android: 'ic_menu_gallery',
-            web: 'gallery',
-          },
-          onPress: async () => {
-            if (!(await requestPhotoAccessIfNeeded())) {
-              return
-            }
+    const items = await openPicker({
+      aspect: [1, 1],
+    })
+    const item = items[0]
+    if (!item) {
+      return
+    }
 
-            const items = await openPicker({
-              aspect: [1, 1],
-            })
-            const item = items[0]
-            if (!item) {
-              return
-            }
+    const croppedImage = await openCropper({
+      mediaType: 'photo',
+      cropperCircleOverlay: true,
+      height: item.height,
+      width: item.width,
+      path: item.path,
+    })
 
-            const croppedImage = await openCropper({
-              mediaType: 'photo',
-              cropperCircleOverlay: true,
-              height: item.height,
-              width: item.width,
-              path: item.path,
-            })
+    onSelectNewAvatar(croppedImage)
+  }, [onSelectNewAvatar, requestPhotoAccessIfNeeded])
 
-            onSelectNewAvatar(croppedImage)
-          },
-        },
-        !!avatar && {
-          label: 'separator',
-        },
-        !!avatar && {
-          testID: 'changeAvatarRemoveBtn',
-          label: _(msg`Remove`),
-          icon: {
-            ios: {
-              name: 'trash',
-            },
-            android: 'ic_delete',
-            web: ['far', 'trash-can'],
-          },
-          onPress: async () => {
-            onSelectNewAvatar(null)
-          },
-        },
-      ].filter(Boolean) as DropdownItem[],
-    [
-      avatar,
-      onSelectNewAvatar,
-      requestCameraAccessIfNeeded,
-      requestPhotoAccessIfNeeded,
-      _,
-    ],
-  )
+  const onRemoveAvatar = React.useCallback(() => {
+    onSelectNewAvatar(null)
+  }, [onSelectNewAvatar])
 
   return (
-    <NativeDropdown
-      testID="changeAvatarBtn"
-      items={dropdownItems}
-      accessibilityLabel={_(msg`Image options`)}
-      accessibilityHint="">
-      {avatar ? (
-        <HighPriorityImage
-          testID="userAvatarImage"
-          style={aviStyle}
-          source={{uri: avatar}}
-          accessibilityRole="image"
-        />
-      ) : (
-        <DefaultAvatar type={type} size={size} />
-      )}
-      <View style={[styles.editButtonContainer, pal.btn]}>
-        <FontAwesomeIcon
-          icon="camera"
-          size={12}
-          color={pal.text.color as string}
-        />
-      </View>
-    </NativeDropdown>
+    <Menu.Root>
+      <Menu.Trigger label={_(msg`Edit avatar`)}>
+        {({props}) => (
+          <TouchableOpacity
+            {...props}
+            activeOpacity={0.8}
+            testID="changeAvatarBtn">
+            {avatar ? (
+              <HighPriorityImage
+                testID="userAvatarImage"
+                style={aviStyle}
+                source={{uri: avatar}}
+                accessibilityRole="image"
+              />
+            ) : (
+              <DefaultAvatar type={type} size={size} />
+            )}
+            <View style={[styles.editButtonContainer, pal.btn]}>
+              <CameraFilled height={14} width={14} style={t.atoms.text} />
+            </View>
+          </TouchableOpacity>
+        )}
+      </Menu.Trigger>
+      <Menu.Outer showCancel>
+        <Menu.Group>
+          {isNative && (
+            <Menu.Item
+              testID="changeAvatarCameraBtn"
+              label={_(msg`Upload from Camera`)}
+              onPress={onOpenCamera}>
+              <Menu.ItemText>
+                <Trans>Upload from Camera</Trans>
+              </Menu.ItemText>
+              <Menu.ItemIcon icon={Camera} />
+            </Menu.Item>
+          )}
+
+          <Menu.Item
+            testID="changeAvatarLibraryBtn"
+            label={_(msg`Upload from Library`)}
+            onPress={onOpenLibrary}>
+            <Menu.ItemText>
+              {isNative ? (
+                <Trans>Upload from Library</Trans>
+              ) : (
+                <Trans>Upload from Files</Trans>
+              )}
+            </Menu.ItemText>
+            <Menu.ItemIcon icon={Library} />
+          </Menu.Item>
+        </Menu.Group>
+        {!!avatar && (
+          <>
+            <Menu.Divider />
+            <Menu.Group>
+              <Menu.Item
+                testID="changeAvatarRemoveBtn"
+                label={_(`Remove Avatar`)}
+                onPress={onRemoveAvatar}>
+                <Menu.ItemText>
+                  <Trans>Remove Avatar</Trans>
+                </Menu.ItemText>
+                <Menu.ItemIcon icon={Trash} />
+              </Menu.Item>
+            </Menu.Group>
+          </>
+        )}
+      </Menu.Outer>
+    </Menu.Root>
   )
 }
 EditableUserAvatar = memo(EditableUserAvatar)
diff --git a/src/view/com/util/UserBanner.tsx b/src/view/com/util/UserBanner.tsx
index cb47b6659..4d73b853b 100644
--- a/src/view/com/util/UserBanner.tsx
+++ b/src/view/com/util/UserBanner.tsx
@@ -1,145 +1,160 @@
-import React, {useMemo} from 'react'
-import {StyleSheet, View} from 'react-native'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import React from 'react'
+import {StyleSheet, TouchableOpacity, View} from 'react-native'
 import {ModerationUI} from '@atproto/api'
 import {Image} from 'expo-image'
 import {useLingui} from '@lingui/react'
-import {msg} from '@lingui/macro'
+import {msg, Trans} from '@lingui/macro'
+
 import {colors} from 'lib/styles'
 import {useTheme} from 'lib/ThemeContext'
+import {useTheme as useAlfTheme, tokens} from '#/alf'
 import {openCamera, openCropper, openPicker} from '../../../lib/media/picker'
 import {
   usePhotoLibraryPermission,
   useCameraPermission,
 } from 'lib/hooks/usePermissions'
 import {usePalette} from 'lib/hooks/usePalette'
-import {isWeb, isAndroid} from 'platform/detection'
+import {isAndroid, isNative} from 'platform/detection'
 import {Image as RNImage} from 'react-native-image-crop-picker'
-import {NativeDropdown, DropdownItem} from './forms/NativeDropdown'
+import {EventStopper} from 'view/com/util/EventStopper'
+import * as Menu from '#/components/Menu'
+import {
+  Camera_Filled_Stroke2_Corner0_Rounded as CameraFilled,
+  Camera_Stroke2_Corner0_Rounded as Camera,
+} from '#/components/icons/Camera'
+import {StreamingLive_Stroke2_Corner0_Rounded as Library} from '#/components/icons/StreamingLive'
+import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
 
 export function UserBanner({
+  type,
   banner,
   moderation,
   onSelectNewBanner,
 }: {
+  type?: 'labeler' | 'default'
   banner?: string | null
   moderation?: ModerationUI
   onSelectNewBanner?: (img: RNImage | null) => void
 }) {
   const pal = usePalette('default')
   const theme = useTheme()
+  const t = useAlfTheme()
   const {_} = useLingui()
   const {requestCameraAccessIfNeeded} = useCameraPermission()
   const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
 
-  const dropdownItems: DropdownItem[] = useMemo(
-    () =>
-      [
-        !isWeb && {
-          testID: 'changeBannerCameraBtn',
-          label: _(msg`Camera`),
-          icon: {
-            ios: {
-              name: 'camera',
-            },
-            android: 'ic_menu_camera',
-            web: 'camera',
-          },
-          onPress: async () => {
-            if (!(await requestCameraAccessIfNeeded())) {
-              return
-            }
-            onSelectNewBanner?.(
-              await openCamera({
-                width: 3000,
-                height: 1000,
-              }),
-            )
-          },
-        },
-        {
-          testID: 'changeBannerLibraryBtn',
-          label: _(msg`Library`),
-          icon: {
-            ios: {
-              name: 'photo.on.rectangle.angled',
-            },
-            android: 'ic_menu_gallery',
-            web: 'gallery',
-          },
-          onPress: async () => {
-            if (!(await requestPhotoAccessIfNeeded())) {
-              return
-            }
-            const items = await openPicker()
-            if (!items[0]) {
-              return
-            }
+  const onOpenCamera = React.useCallback(async () => {
+    if (!(await requestCameraAccessIfNeeded())) {
+      return
+    }
+    onSelectNewBanner?.(
+      await openCamera({
+        width: 3000,
+        height: 1000,
+      }),
+    )
+  }, [onSelectNewBanner, requestCameraAccessIfNeeded])
 
-            onSelectNewBanner?.(
-              await openCropper({
-                mediaType: 'photo',
-                path: items[0].path,
-                width: 3000,
-                height: 1000,
-              }),
-            )
-          },
-        },
-        !!banner && {
-          testID: 'changeBannerRemoveBtn',
-          label: _(msg`Remove`),
-          icon: {
-            ios: {
-              name: 'trash',
-            },
-            android: 'ic_delete',
-            web: ['far', 'trash-can'],
-          },
-          onPress: () => {
-            onSelectNewBanner?.(null)
-          },
-        },
-      ].filter(Boolean) as DropdownItem[],
-    [
-      banner,
-      onSelectNewBanner,
-      requestCameraAccessIfNeeded,
-      requestPhotoAccessIfNeeded,
-      _,
-    ],
-  )
+  const onOpenLibrary = React.useCallback(async () => {
+    if (!(await requestPhotoAccessIfNeeded())) {
+      return
+    }
+    const items = await openPicker()
+    if (!items[0]) {
+      return
+    }
+
+    onSelectNewBanner?.(
+      await openCropper({
+        mediaType: 'photo',
+        path: items[0].path,
+        width: 3000,
+        height: 1000,
+      }),
+    )
+  }, [onSelectNewBanner, requestPhotoAccessIfNeeded])
+
+  const onRemoveBanner = React.useCallback(() => {
+    onSelectNewBanner?.(null)
+  }, [onSelectNewBanner])
 
   // setUserBanner is only passed as prop on the EditProfile component
   return onSelectNewBanner ? (
-    <NativeDropdown
-      testID="changeBannerBtn"
-      items={dropdownItems}
-      accessibilityLabel={_(msg`Image options`)}
-      accessibilityHint="">
-      {banner ? (
-        <Image
-          testID="userBannerImage"
-          style={styles.bannerImage}
-          source={{uri: banner}}
-          accessible={true}
-          accessibilityIgnoresInvertColors
-        />
-      ) : (
-        <View
-          testID="userBannerFallback"
-          style={[styles.bannerImage, styles.defaultBanner]}
-        />
-      )}
-      <View style={[styles.editButtonContainer, pal.btn]}>
-        <FontAwesomeIcon
-          icon="camera"
-          size={12}
-          style={{color: colors.white}}
-          color={pal.text.color as string}
-        />
-      </View>
-    </NativeDropdown>
+    <EventStopper onKeyDown={false}>
+      <Menu.Root>
+        <Menu.Trigger label={_(msg`Edit avatar`)}>
+          {({props}) => (
+            <TouchableOpacity
+              {...props}
+              activeOpacity={0.8}
+              testID="changeBannerBtn">
+              {banner ? (
+                <Image
+                  testID="userBannerImage"
+                  style={styles.bannerImage}
+                  source={{uri: banner}}
+                  accessible={true}
+                  accessibilityIgnoresInvertColors
+                />
+              ) : (
+                <View
+                  testID="userBannerFallback"
+                  style={[styles.bannerImage, styles.defaultBanner]}
+                />
+              )}
+              <View style={[styles.editButtonContainer, pal.btn]}>
+                <CameraFilled height={14} width={14} style={t.atoms.text} />
+              </View>
+            </TouchableOpacity>
+          )}
+        </Menu.Trigger>
+        <Menu.Outer showCancel>
+          <Menu.Group>
+            {isNative && (
+              <Menu.Item
+                testID="changeBannerCameraBtn"
+                label={_(msg`Upload from Camera`)}
+                onPress={onOpenCamera}>
+                <Menu.ItemText>
+                  <Trans>Upload from Camera</Trans>
+                </Menu.ItemText>
+                <Menu.ItemIcon icon={Camera} />
+              </Menu.Item>
+            )}
+
+            <Menu.Item
+              testID="changeBannerLibraryBtn"
+              label={_(msg`Upload from Library`)}
+              onPress={onOpenLibrary}>
+              <Menu.ItemText>
+                {isNative ? (
+                  <Trans>Upload from Library</Trans>
+                ) : (
+                  <Trans>Upload from Files</Trans>
+                )}
+              </Menu.ItemText>
+              <Menu.ItemIcon icon={Library} />
+            </Menu.Item>
+          </Menu.Group>
+          {!!banner && (
+            <>
+              <Menu.Divider />
+              <Menu.Group>
+                <Menu.Item
+                  testID="changeBannerRemoveBtn"
+                  label={_(`Remove Banner`)}
+                  onPress={onRemoveBanner}>
+                  <Menu.ItemText>
+                    <Trans>Remove Banner</Trans>
+                  </Menu.ItemText>
+                  <Menu.ItemIcon icon={Trash} />
+                </Menu.Item>
+              </Menu.Group>
+            </>
+          )}
+        </Menu.Outer>
+      </Menu.Root>
+    </EventStopper>
   ) : banner &&
     !((moderation?.blur && isAndroid) /* android crashes with blur */) ? (
     <Image
@@ -157,7 +172,10 @@ export function UserBanner({
   ) : (
     <View
       testID="userBannerFallback"
-      style={[styles.bannerImage, styles.defaultBanner]}
+      style={[
+        styles.bannerImage,
+        type === 'labeler' ? styles.labelerBanner : styles.defaultBanner,
+      ]}
     />
   )
 }
@@ -181,4 +199,7 @@ const styles = StyleSheet.create({
   defaultBanner: {
     backgroundColor: '#0070ff',
   },
+  labelerBanner: {
+    backgroundColor: tokens.color.temp_purple,
+  },
 })
diff --git a/src/view/com/util/ViewHeader.tsx b/src/view/com/util/ViewHeader.tsx
index 1ccfcf56c..872e10eef 100644
--- a/src/view/com/util/ViewHeader.tsx
+++ b/src/view/com/util/ViewHeader.tsx
@@ -13,11 +13,13 @@ import Animated from 'react-native-reanimated'
 import {useSetDrawerOpen} from '#/state/shell'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
+import {useTheme} from '#/alf'
 
 const BACK_HITSLOP = {left: 20, top: 20, right: 50, bottom: 20}
 
 export function ViewHeader({
   title,
+  subtitle,
   canGoBack,
   showBackButton = true,
   hideOnScroll,
@@ -26,6 +28,7 @@ export function ViewHeader({
   renderButton,
 }: {
   title: string
+  subtitle?: string
   canGoBack?: boolean
   showBackButton?: boolean
   hideOnScroll?: boolean
@@ -39,6 +42,7 @@ export function ViewHeader({
   const navigation = useNavigation<NavigationProp>()
   const {track} = useAnalytics()
   const {isDesktop, isTablet} = useWebMediaQueries()
+  const t = useTheme()
 
   const onPressBack = React.useCallback(() => {
     if (navigation.canGoBack()) {
@@ -71,42 +75,60 @@ export function ViewHeader({
 
     return (
       <Container hideOnScroll={hideOnScroll || false} showBorder={showBorder}>
-        {showBackButton ? (
-          <TouchableOpacity
-            testID="viewHeaderDrawerBtn"
-            onPress={canGoBack ? onPressBack : onPressMenu}
-            hitSlop={BACK_HITSLOP}
-            style={canGoBack ? styles.backBtn : styles.backBtnWide}
-            accessibilityRole="button"
-            accessibilityLabel={canGoBack ? _(msg`Back`) : _(msg`Menu`)}
-            accessibilityHint={
-              canGoBack ? '' : _(msg`Access navigation links and settings`)
-            }>
-            {canGoBack ? (
-              <FontAwesomeIcon
-                size={18}
-                icon="angle-left"
-                style={[styles.backIcon, pal.text]}
-              />
-            ) : !isTablet ? (
-              <FontAwesomeIcon
-                size={18}
-                icon="bars"
-                style={[styles.backIcon, pal.textLight]}
-              />
+        <View style={{flex: 1}}>
+          <View style={{flexDirection: 'row', alignItems: 'center'}}>
+            {showBackButton ? (
+              <TouchableOpacity
+                testID="viewHeaderDrawerBtn"
+                onPress={canGoBack ? onPressBack : onPressMenu}
+                hitSlop={BACK_HITSLOP}
+                style={canGoBack ? styles.backBtn : styles.backBtnWide}
+                accessibilityRole="button"
+                accessibilityLabel={canGoBack ? _(msg`Back`) : _(msg`Menu`)}
+                accessibilityHint={
+                  canGoBack ? '' : _(msg`Access navigation links and settings`)
+                }>
+                {canGoBack ? (
+                  <FontAwesomeIcon
+                    size={18}
+                    icon="angle-left"
+                    style={[styles.backIcon, pal.text]}
+                  />
+                ) : !isTablet ? (
+                  <FontAwesomeIcon
+                    size={18}
+                    icon="bars"
+                    style={[styles.backIcon, pal.textLight]}
+                  />
+                ) : null}
+              </TouchableOpacity>
             ) : null}
-          </TouchableOpacity>
-        ) : null}
-        <View style={styles.titleContainer} pointerEvents="none">
-          <Text type="title" style={[pal.text, styles.title]}>
-            {title}
-          </Text>
+            <View style={styles.titleContainer} pointerEvents="none">
+              <Text type="title" style={[pal.text, styles.title]}>
+                {title}
+              </Text>
+            </View>
+            {renderButton ? (
+              renderButton()
+            ) : showBackButton ? (
+              <View style={canGoBack ? styles.backBtn : styles.backBtnWide} />
+            ) : null}
+          </View>
+          {subtitle ? (
+            <View
+              style={[styles.titleContainer, {marginTop: -3}]}
+              pointerEvents="none">
+              <Text
+                style={[
+                  pal.text,
+                  styles.subtitle,
+                  t.atoms.text_contrast_medium,
+                ]}>
+                {subtitle}
+              </Text>
+            </View>
+          ) : undefined}
         </View>
-        {renderButton ? (
-          renderButton()
-        ) : showBackButton ? (
-          <View style={canGoBack ? styles.backBtn : styles.backBtnWide} />
-        ) : null}
       </Container>
     )
   }
@@ -185,7 +207,6 @@ function Container({
 const styles = StyleSheet.create({
   header: {
     flexDirection: 'row',
-    alignItems: 'center',
     paddingHorizontal: 12,
     paddingVertical: 6,
     width: '100%',
@@ -207,12 +228,14 @@ const styles = StyleSheet.create({
   titleContainer: {
     marginLeft: 'auto',
     marginRight: 'auto',
-    paddingRight: 10,
+    alignItems: 'center',
   },
   title: {
     fontWeight: 'bold',
   },
-
+  subtitle: {
+    fontSize: 13,
+  },
   backBtn: {
     width: 30,
     height: 30,
diff --git a/src/view/com/util/Views.d.ts b/src/view/com/util/Views.d.ts
index 6a90cc229..16713921f 100644
--- a/src/view/com/util/Views.d.ts
+++ b/src/view/com/util/Views.d.ts
@@ -5,4 +5,6 @@ export function CenteredView({
   style,
   sideBorders,
   ...props
-}: React.PropsWithChildren<ViewProps & {sideBorders?: boolean}>)
+}: React.PropsWithChildren<
+  ViewProps & {sideBorders?: boolean; topBorder?: boolean}
+>)
diff --git a/src/view/com/util/Views.web.tsx b/src/view/com/util/Views.web.tsx
index db3b9de0d..ae165077c 100644
--- a/src/view/com/util/Views.web.tsx
+++ b/src/view/com/util/Views.web.tsx
@@ -32,8 +32,11 @@ interface AddedProps {
 export function CenteredView({
   style,
   sideBorders,
+  topBorder,
   ...props
-}: React.PropsWithChildren<ViewProps & {sideBorders?: boolean}>) {
+}: React.PropsWithChildren<
+  ViewProps & {sideBorders?: boolean; topBorder?: boolean}
+>) {
   const pal = usePalette('default')
   const {isMobile} = useWebMediaQueries()
   if (!isMobile) {
@@ -46,6 +49,12 @@ export function CenteredView({
     })
     style = addStyle(style, pal.border)
   }
+  if (topBorder) {
+    style = addStyle(style, {
+      borderTopWidth: 1,
+    })
+    style = addStyle(style, pal.border)
+  }
   return <View style={style} {...props} />
 }
 
diff --git a/src/view/com/util/forms/DateInput.tsx b/src/view/com/util/forms/DateInput.tsx
index c5f0afc8f..0104562aa 100644
--- a/src/view/com/util/forms/DateInput.tsx
+++ b/src/view/com/util/forms/DateInput.tsx
@@ -1,8 +1,5 @@
 import React, {useState, useCallback} from 'react'
 import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native'
-import DateTimePicker, {
-  DateTimePickerEvent,
-} from '@react-native-community/datetimepicker'
 import {
   FontAwesomeIcon,
   FontAwesomeIconStyle,
@@ -14,6 +11,7 @@ import {TypographyVariant} from 'lib/ThemeContext'
 import {useTheme} from 'lib/ThemeContext'
 import {usePalette} from 'lib/hooks/usePalette'
 import {getLocales} from 'expo-localization'
+import DatePicker from 'react-native-date-picker'
 
 const LOCALE = getLocales()[0]
 
@@ -43,11 +41,9 @@ export function DateInput(props: Props) {
   }, [props.handleAsUTC])
 
   const onChangeInternal = useCallback(
-    (event: DateTimePickerEvent, date: Date | undefined) => {
+    (date: Date) => {
       setShow(false)
-      if (date) {
-        props.onChange(date)
-      }
+      props.onChange(date)
     },
     [setShow, props],
   )
@@ -56,6 +52,10 @@ export function DateInput(props: Props) {
     setShow(true)
   }, [setShow])
 
+  const onCancel = useCallback(() => {
+    setShow(false)
+  }, [])
+
   return (
     <View>
       {isAndroid && (
@@ -80,15 +80,17 @@ export function DateInput(props: Props) {
         </Button>
       )}
       {(isIOS || show) && (
-        <DateTimePicker
-          testID={props.testID ? `${props.testID}-datepicker` : undefined}
+        <DatePicker
+          timeZoneOffsetInMinutes={0}
+          modal={isAndroid}
+          open={isAndroid}
+          theme={theme.colorScheme}
+          date={props.value}
+          onDateChange={onChangeInternal}
+          onConfirm={onChangeInternal}
+          onCancel={onCancel}
           mode="date"
-          timeZoneName={props.handleAsUTC ? 'Etc/UTC' : undefined}
-          display="spinner"
-          // @ts-ignore applies in iOS only -prf
-          themeVariant={theme.colorScheme}
-          value={props.value}
-          onChange={onChangeInternal}
+          testID={props.testID ? `${props.testID}-datepicker` : undefined}
           accessibilityLabel={props.accessibilityLabel}
           accessibilityHint={props.accessibilityHint}
           accessibilityLabelledBy={props.accessibilityLabelledBy}
diff --git a/src/view/com/util/forms/NativeDropdown.tsx b/src/view/com/util/forms/NativeDropdown.tsx
index 082285064..0a47569f2 100644
--- a/src/view/com/util/forms/NativeDropdown.tsx
+++ b/src/view/com/util/forms/NativeDropdown.tsx
@@ -1,7 +1,7 @@
 import React from 'react'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import * as DropdownMenu from 'zeego/dropdown-menu'
-import {Pressable, StyleSheet, Platform, View} from 'react-native'
+import {Pressable, StyleSheet, Platform, View, ViewStyle} from 'react-native'
 import {IconProp} from '@fortawesome/fontawesome-svg-core'
 import {MenuItemCommonProps} from 'zeego/lib/typescript/menu'
 import {usePalette} from 'lib/hooks/usePalette'
@@ -151,6 +151,7 @@ type Props = {
   testID?: string
   accessibilityLabel?: string
   accessibilityHint?: string
+  triggerStyle?: ViewStyle
 }
 
 /* The `NativeDropdown` function uses native iOS and Android dropdown menus.
diff --git a/src/view/com/util/forms/NativeDropdown.web.tsx b/src/view/com/util/forms/NativeDropdown.web.tsx
index 9e9888ad8..94591d393 100644
--- a/src/view/com/util/forms/NativeDropdown.web.tsx
+++ b/src/view/com/util/forms/NativeDropdown.web.tsx
@@ -1,7 +1,7 @@
 import React from 'react'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
-import {Pressable, StyleSheet, View, Text} from 'react-native'
+import {Pressable, StyleSheet, View, Text, ViewStyle} from 'react-native'
 import {IconProp} from '@fortawesome/fontawesome-svg-core'
 import {MenuItemCommonProps} from 'zeego/lib/typescript/menu'
 import {usePalette} from 'lib/hooks/usePalette'
@@ -21,6 +21,7 @@ export const DropdownMenuItem = (props: ItemProps & {testID?: string}) => {
 
   return (
     <DropdownMenu.Item
+      className="nativeDropdown-item"
       {...props}
       style={StyleSheet.flatten([
         styles.item,
@@ -52,6 +53,7 @@ type Props = {
   testID?: string
   accessibilityLabel?: string
   accessibilityHint?: string
+  triggerStyle?: ViewStyle
 }
 
 export function NativeDropdown({
@@ -60,6 +62,7 @@ export function NativeDropdown({
   testID,
   accessibilityLabel,
   accessibilityHint,
+  triggerStyle,
 }: React.PropsWithChildren<Props>) {
   const pal = usePalette('default')
   const theme = useTheme()
@@ -119,7 +122,8 @@ export function NativeDropdown({
           accessibilityLabel={accessibilityLabel}
           accessibilityHint={accessibilityHint}
           onPress={() => setOpen(o => !o)}
-          hitSlop={HITSLOP_10}>
+          hitSlop={HITSLOP_10}
+          style={triggerStyle}>
           {children}
         </Pressable>
       </DropdownMenu.Trigger>
@@ -232,6 +236,10 @@ const styles = StyleSheet.create({
     paddingLeft: 12,
     paddingRight: 12,
     borderRadius: 8,
+    fontFamily:
+      '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Liberation Sans", Helvetica, Arial, sans-serif',
+    outline: 0,
+    border: 0,
   },
   itemTitle: {
     fontSize: 16,
diff --git a/src/view/com/util/forms/PostDropdownBtn.tsx b/src/view/com/util/forms/PostDropdownBtn.tsx
index e56c88d2c..d04672c63 100644
--- a/src/view/com/util/forms/PostDropdownBtn.tsx
+++ b/src/view/com/util/forms/PostDropdownBtn.tsx
@@ -1,36 +1,50 @@
 import React, {memo} from 'react'
-import {StyleProp, View, ViewStyle} from 'react-native'
-import Clipboard from '@react-native-clipboard/clipboard'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {Pressable, PressableProps, StyleProp, ViewStyle} from 'react-native'
 import {
   AppBskyActorDefs,
   AppBskyFeedPost,
   AtUri,
   RichText as RichTextAPI,
 } from '@atproto/api'
-import {toShareUrl} from 'lib/strings/url-helpers'
-import {useTheme} from 'lib/ThemeContext'
-import {shareUrl} from 'lib/sharing'
-import {
-  NativeDropdown,
-  DropdownItem as NativeDropdownItem,
-} from './NativeDropdown'
-import * as Toast from '../Toast'
-import {EventStopper} from '../EventStopper'
-import {useModalControls} from '#/state/modals'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import Clipboard from '@react-native-clipboard/clipboard'
+import {useNavigation} from '@react-navigation/native'
+
 import {makeProfileLink} from '#/lib/routes/links'
+import {CommonNavigatorParams} from '#/lib/routes/types'
+import {richTextToString} from '#/lib/strings/rich-text-helpers'
 import {getTranslatorLink} from '#/locale/helpers'
-import {usePostDeleteMutation} from '#/state/queries/post'
+import {logger} from '#/logger'
+import {isWeb} from '#/platform/detection'
 import {useMutedThreads, useToggleThreadMute} from '#/state/muted-threads'
 import {useLanguagePrefs} from '#/state/preferences'
 import {useHiddenPosts, useHiddenPostsApi} from '#/state/preferences'
 import {useOpenLink} from '#/state/preferences/in-app-browser'
-import {logger} from '#/logger'
-import {msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
+import {usePostDeleteMutation} from '#/state/queries/post'
 import {useSession} from '#/state/session'
-import {isWeb} from '#/platform/detection'
-import {richTextToString} from '#/lib/strings/rich-text-helpers'
+import {getCurrentRoute} from 'lib/routes/helpers'
+import {shareUrl} from 'lib/sharing'
+import {toShareUrl} from 'lib/strings/url-helpers'
+import {useTheme} from 'lib/ThemeContext'
+import {atoms as a, useTheme as useAlf} from '#/alf'
+import {useDialogControl} from '#/components/Dialog'
+import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
+import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox'
+import {BubbleQuestion_Stroke2_Corner0_Rounded as Translate} from '#/components/icons/Bubble'
+import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '#/components/icons/Clipboard'
+import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash'
+import {Filter_Stroke2_Corner0_Rounded as Filter} from '#/components/icons/Filter'
+import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute'
+import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker'
+import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
+import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning'
+import * as Menu from '#/components/Menu'
+import * as Prompt from '#/components/Prompt'
+import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog'
+import {EventStopper} from '../EventStopper'
+import * as Toast from '../Toast'
 
 let PostDropdownBtn = ({
   testID,
@@ -40,7 +54,7 @@ let PostDropdownBtn = ({
   record,
   richText,
   style,
-  showAppealLabelItem,
+  hitSlop,
 }: {
   testID: string
   postAuthor: AppBskyActorDefs.ProfileViewBasic
@@ -49,13 +63,13 @@ let PostDropdownBtn = ({
   record: AppBskyFeedPost.Record
   richText: RichTextAPI
   style?: StyleProp<ViewStyle>
-  showAppealLabelItem?: boolean
+  hitSlop?: PressableProps['hitSlop']
 }): React.ReactNode => {
   const {hasSession, currentAccount} = useSession()
   const theme = useTheme()
+  const alf = useAlf()
   const {_} = useLingui()
   const defaultCtrlColor = theme.palette.default.postCtrl
-  const {openModal} = useModalControls()
   const langPrefs = useLanguagePrefs()
   const mutedThreads = useMutedThreads()
   const toggleThreadMute = useToggleThreadMute()
@@ -63,11 +77,18 @@ let PostDropdownBtn = ({
   const hiddenPosts = useHiddenPosts()
   const {hidePost} = useHiddenPostsApi()
   const openLink = useOpenLink()
+  const navigation = useNavigation()
+  const {mutedWordsDialogControl} = useGlobalDialogsControlContext()
+  const reportDialogControl = useReportDialogControl()
+  const deletePromptControl = useDialogControl()
+  const hidePromptControl = useDialogControl()
+  const loggedOutWarningPromptControl = useDialogControl()
 
   const rootUri = record.reply?.root?.uri || postUri
   const isThreadMuted = mutedThreads.includes(rootUri)
   const isPostHidden = hiddenPosts && hiddenPosts.includes(postUri)
   const isAuthor = postAuthor.did === currentAccount?.did
+
   const href = React.useMemo(() => {
     const urip = new AtUri(postUri)
     return makeProfileLink(postAuthor, 'post', urip.rkey)
@@ -82,13 +103,38 @@ let PostDropdownBtn = ({
     postDeleteMutation.mutateAsync({uri: postUri}).then(
       () => {
         Toast.show(_(msg`Post deleted`))
+
+        const route = getCurrentRoute(navigation.getState())
+        if (route.name === 'PostThread') {
+          const params = route.params as CommonNavigatorParams['PostThread']
+          if (
+            currentAccount &&
+            isAuthor &&
+            (params.name === currentAccount.handle ||
+              params.name === currentAccount.did)
+          ) {
+            const currentHref = makeProfileLink(postAuthor, 'post', params.rkey)
+            if (currentHref === href && navigation.canGoBack()) {
+              navigation.goBack()
+            }
+          }
+        }
       },
       e => {
         logger.error('Failed to delete post', {message: e})
         Toast.show(_(msg`Failed to delete post, please try again`))
       },
     )
-  }, [postUri, postDeleteMutation, _])
+  }, [
+    navigation,
+    postUri,
+    postDeleteMutation,
+    postAuthor,
+    currentAccount,
+    isAuthor,
+    href,
+    _,
+  ])
 
   const onToggleThreadMute = React.useCallback(() => {
     try {
@@ -120,159 +166,186 @@ let PostDropdownBtn = ({
     hidePost({uri: postUri})
   }, [postUri, hidePost])
 
-  const dropdownItems: NativeDropdownItem[] = [
-    {
-      label: _(msg`Translate`),
-      onPress() {
-        onOpenTranslate()
-      },
-      testID: 'postDropdownTranslateBtn',
-      icon: {
-        ios: {
-          name: 'character.book.closed',
-        },
-        android: 'ic_menu_sort_alphabetically',
-        web: 'language',
-      },
-    },
-    {
-      label: _(msg`Copy post text`),
-      onPress() {
-        onCopyPostText()
-      },
-      testID: 'postDropdownCopyTextBtn',
-      icon: {
-        ios: {
-          name: 'doc.on.doc',
-        },
-        android: 'ic_menu_edit',
-        web: ['far', 'paste'],
-      },
-    },
-    {
-      label: isWeb ? _(msg`Copy link to post`) : _(msg`Share`),
-      onPress() {
-        const url = toShareUrl(href)
-        shareUrl(url)
-      },
-      testID: 'postDropdownShareBtn',
-      icon: {
-        ios: {
-          name: 'square.and.arrow.up',
-        },
-        android: 'ic_menu_share',
-        web: 'share',
-      },
-    },
-    hasSession && {
-      label: 'separator',
-    },
-    hasSession && {
-      label: isThreadMuted ? _(msg`Unmute thread`) : _(msg`Mute thread`),
-      onPress() {
-        onToggleThreadMute()
-      },
-      testID: 'postDropdownMuteThreadBtn',
-      icon: {
-        ios: {
-          name: 'speaker.slash',
-        },
-        android: 'ic_lock_silent_mode',
-        web: 'comment-slash',
-      },
-    },
-    hasSession &&
-      !isAuthor &&
-      !isPostHidden && {
-        label: _(msg`Hide post`),
-        onPress() {
-          openModal({
-            name: 'confirm',
-            title: _(msg`Hide this post?`),
-            message: _(msg`This will hide this post from your feeds.`),
-            onPressConfirm: onHidePost,
-          })
-        },
-        testID: 'postDropdownHideBtn',
-        icon: {
-          ios: {
-            name: 'eye.slash',
-          },
-          android: 'ic_menu_delete',
-          web: ['far', 'eye-slash'],
-        },
-      },
-    {
-      label: 'separator',
-    },
-    !isAuthor &&
-      hasSession && {
-        label: _(msg`Report post`),
-        onPress() {
-          openModal({
-            name: 'report',
-            uri: postUri,
-            cid: postCid,
-          })
-        },
-        testID: 'postDropdownReportBtn',
-        icon: {
-          ios: {
-            name: 'exclamationmark.triangle',
-          },
-          android: 'ic_menu_report_image',
-          web: 'circle-exclamation',
-        },
-      },
-    isAuthor && {
-      label: _(msg`Delete post`),
-      onPress() {
-        openModal({
-          name: 'confirm',
-          title: _(msg`Delete this post?`),
-          message: _(msg`Are you sure? This cannot be undone.`),
-          onPressConfirm: onDeletePost,
-        })
-      },
-      testID: 'postDropdownDeleteBtn',
-      icon: {
-        ios: {
-          name: 'trash',
-        },
-        android: 'ic_menu_delete',
-        web: ['far', 'trash-can'],
-      },
-    },
-    showAppealLabelItem && {
-      label: 'separator',
-    },
-    showAppealLabelItem && {
-      label: _(msg`Appeal content warning`),
-      onPress() {
-        openModal({name: 'appeal-label', uri: postUri, cid: postCid})
-      },
-      testID: 'postDropdownAppealBtn',
-      icon: {
-        ios: {
-          name: 'exclamationmark.triangle',
-        },
-        android: 'ic_menu_report_image',
-        web: 'circle-exclamation',
-      },
-    },
-  ].filter(Boolean) as NativeDropdownItem[]
+  const shouldShowLoggedOutWarning = React.useMemo(() => {
+    return !!postAuthor.labels?.find(
+      label => label.val === '!no-unauthenticated',
+    )
+  }, [postAuthor])
+
+  const onSharePost = React.useCallback(() => {
+    const url = toShareUrl(href)
+    shareUrl(url)
+  }, [href])
 
   return (
-    <EventStopper>
-      <NativeDropdown
-        testID={testID}
-        items={dropdownItems}
-        accessibilityLabel={_(msg`More post options`)}
-        accessibilityHint="">
-        <View style={style}>
-          <FontAwesomeIcon icon="ellipsis" size={20} color={defaultCtrlColor} />
-        </View>
-      </NativeDropdown>
+    <EventStopper onKeyDown={false}>
+      <Menu.Root>
+        <Menu.Trigger label={_(msg`Open post options menu`)}>
+          {({props, state}) => {
+            return (
+              <Pressable
+                {...props}
+                hitSlop={hitSlop}
+                testID={testID}
+                style={[
+                  style,
+                  a.rounded_full,
+                  (state.hovered || state.pressed) && [
+                    alf.atoms.bg_contrast_50,
+                  ],
+                ]}>
+                <FontAwesomeIcon
+                  icon="ellipsis"
+                  size={20}
+                  color={defaultCtrlColor}
+                  style={{pointerEvents: 'none'}}
+                />
+              </Pressable>
+            )
+          }}
+        </Menu.Trigger>
+
+        <Menu.Outer>
+          <Menu.Group>
+            <Menu.Item
+              testID="postDropdownTranslateBtn"
+              label={_(msg`Translate`)}
+              onPress={onOpenTranslate}>
+              <Menu.ItemText>{_(msg`Translate`)}</Menu.ItemText>
+              <Menu.ItemIcon icon={Translate} position="right" />
+            </Menu.Item>
+
+            <Menu.Item
+              testID="postDropdownCopyTextBtn"
+              label={_(msg`Copy post text`)}
+              onPress={onCopyPostText}>
+              <Menu.ItemText>{_(msg`Copy post text`)}</Menu.ItemText>
+              <Menu.ItemIcon icon={ClipboardIcon} position="right" />
+            </Menu.Item>
+
+            <Menu.Item
+              testID="postDropdownShareBtn"
+              label={isWeb ? _(msg`Copy link to post`) : _(msg`Share`)}
+              onPress={() => {
+                if (shouldShowLoggedOutWarning) {
+                  loggedOutWarningPromptControl.open()
+                } else {
+                  onSharePost()
+                }
+              }}>
+              <Menu.ItemText>
+                {isWeb ? _(msg`Copy link to post`) : _(msg`Share`)}
+              </Menu.ItemText>
+              <Menu.ItemIcon icon={Share} position="right" />
+            </Menu.Item>
+          </Menu.Group>
+
+          {hasSession && (
+            <>
+              <Menu.Divider />
+
+              <Menu.Group>
+                <Menu.Item
+                  testID="postDropdownMuteThreadBtn"
+                  label={
+                    isThreadMuted ? _(msg`Unmute thread`) : _(msg`Mute thread`)
+                  }
+                  onPress={onToggleThreadMute}>
+                  <Menu.ItemText>
+                    {isThreadMuted
+                      ? _(msg`Unmute thread`)
+                      : _(msg`Mute thread`)}
+                  </Menu.ItemText>
+                  <Menu.ItemIcon
+                    icon={isThreadMuted ? Unmute : Mute}
+                    position="right"
+                  />
+                </Menu.Item>
+
+                <Menu.Item
+                  testID="postDropdownMuteWordsBtn"
+                  label={_(msg`Mute words & tags`)}
+                  onPress={() => mutedWordsDialogControl.open()}>
+                  <Menu.ItemText>{_(msg`Mute words & tags`)}</Menu.ItemText>
+                  <Menu.ItemIcon icon={Filter} position="right" />
+                </Menu.Item>
+
+                {!isAuthor && !isPostHidden && (
+                  <Menu.Item
+                    testID="postDropdownHideBtn"
+                    label={_(msg`Hide post`)}
+                    onPress={hidePromptControl.open}>
+                    <Menu.ItemText>{_(msg`Hide post`)}</Menu.ItemText>
+                    <Menu.ItemIcon icon={EyeSlash} position="right" />
+                  </Menu.Item>
+                )}
+              </Menu.Group>
+            </>
+          )}
+
+          <Menu.Divider />
+
+          <Menu.Group>
+            {!isAuthor && (
+              <Menu.Item
+                testID="postDropdownReportBtn"
+                label={_(msg`Report post`)}
+                onPress={() => reportDialogControl.open()}>
+                <Menu.ItemText>{_(msg`Report post`)}</Menu.ItemText>
+                <Menu.ItemIcon icon={Warning} position="right" />
+              </Menu.Item>
+            )}
+
+            {isAuthor && (
+              <Menu.Item
+                testID="postDropdownDeleteBtn"
+                label={_(msg`Delete post`)}
+                onPress={deletePromptControl.open}>
+                <Menu.ItemText>{_(msg`Delete post`)}</Menu.ItemText>
+                <Menu.ItemIcon icon={Trash} position="right" />
+              </Menu.Item>
+            )}
+          </Menu.Group>
+        </Menu.Outer>
+      </Menu.Root>
+
+      <Prompt.Basic
+        control={deletePromptControl}
+        title={_(msg`Delete this post?`)}
+        description={_(
+          msg`If you remove this post, you won't be able to recover it.`,
+        )}
+        onConfirm={onDeletePost}
+        confirmButtonCta={_(msg`Delete`)}
+        confirmButtonColor="negative"
+      />
+
+      <Prompt.Basic
+        control={hidePromptControl}
+        title={_(msg`Hide this post?`)}
+        description={_(msg`This post will be hidden from feeds.`)}
+        onConfirm={onHidePost}
+        confirmButtonCta={_(msg`Hide`)}
+      />
+
+      <ReportDialog
+        control={reportDialogControl}
+        params={{
+          type: 'post',
+          uri: postUri,
+          cid: postCid,
+        }}
+      />
+
+      <Prompt.Basic
+        control={loggedOutWarningPromptControl}
+        title={_(msg`Note about sharing`)}
+        description={_(
+          msg`This post is only visible to logged-in users. It won't be visible to people who aren't logged in.`,
+        )}
+        onConfirm={onSharePost}
+        confirmButtonCta={_(msg`Share anyway`)}
+      />
     </EventStopper>
   )
 }
diff --git a/src/view/com/util/forms/SelectableBtn.tsx b/src/view/com/util/forms/SelectableBtn.tsx
index f09d063a1..e577e155d 100644
--- a/src/view/com/util/forms/SelectableBtn.tsx
+++ b/src/view/com/util/forms/SelectableBtn.tsx
@@ -57,6 +57,7 @@ const styles = StyleSheet.create({
   btn: {
     flexDirection: 'row',
     justifyContent: 'center',
+    flexGrow: 1,
     borderWidth: 1,
     borderLeftWidth: 0,
     paddingHorizontal: 10,
diff --git a/src/view/com/util/layouts/LoggedOutLayout.tsx b/src/view/com/util/layouts/LoggedOutLayout.tsx
index 9424a7154..0272a44c6 100644
--- a/src/view/com/util/layouts/LoggedOutLayout.tsx
+++ b/src/view/com/util/layouts/LoggedOutLayout.tsx
@@ -1,19 +1,24 @@
 import React from 'react'
-import {StyleSheet, View} from 'react-native'
-import {Text} from '../text/Text'
+import {ScrollView, StyleSheet, View} from 'react-native'
+
+import {isWeb} from '#/platform/detection'
+import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
+import {atoms as a} from '#/alf'
+import {Text} from '../text/Text'
 
 export const LoggedOutLayout = ({
   leadin,
   title,
   description,
   children,
+  scrollable,
 }: React.PropsWithChildren<{
   leadin: string
   title: string
   description: string
+  scrollable?: boolean
 }>) => {
   const {isMobile, isTabletOrMobile} = useWebMediaQueries()
   const pal = usePalette('default')
@@ -25,7 +30,18 @@ export const LoggedOutLayout = ({
   })
 
   if (isMobile) {
-    return <View style={{paddingTop: 10}}>{children}</View>
+    if (scrollable) {
+      return (
+        <ScrollView
+          style={styles.scrollview}
+          keyboardShouldPersistTaps="handled"
+          keyboardDismissMode="on-drag">
+          <View style={a.pt_md}>{children}</View>
+        </ScrollView>
+      )
+    } else {
+      return <View style={a.pt_md}>{children}</View>
+    }
   }
   return (
     <View style={styles.container}>
@@ -50,9 +66,23 @@ export const LoggedOutLayout = ({
           {description}
         </Text>
       </View>
-      <View style={[styles.content, contentBg]}>
-        <View style={styles.contentWrapper}>{children}</View>
-      </View>
+      {scrollable ? (
+        <View style={[styles.scrollableContent, contentBg]}>
+          <ScrollView
+            style={styles.scrollview}
+            contentContainerStyle={styles.scrollViewContentContainer}
+            keyboardShouldPersistTaps="handled"
+            keyboardDismissMode="on-drag">
+            <View style={[styles.contentWrapper, isWeb && a.my_auto]}>
+              {children}
+            </View>
+          </ScrollView>
+        </View>
+      ) : (
+        <View style={[styles.content, contentBg]}>
+          <View style={styles.contentWrapper}>{children}</View>
+        </View>
+      )}
     </View>
   )
 }
@@ -74,7 +104,16 @@ const styles = StyleSheet.create({
     paddingHorizontal: 40,
     justifyContent: 'center',
   },
-
+  scrollableContent: {
+    flex: 2,
+  },
+  scrollview: {
+    flex: 1,
+  },
+  scrollViewContentContainer: {
+    flex: 1,
+    paddingHorizontal: 40,
+  },
   leadinText: {
     fontSize: 36,
     fontWeight: '800',
diff --git a/src/view/com/util/load-latest/LoadLatestBtn.tsx b/src/view/com/util/load-latest/LoadLatestBtn.tsx
index 5fad11760..f02e4a2bd 100644
--- a/src/view/com/util/load-latest/LoadLatestBtn.tsx
+++ b/src/view/com/util/load-latest/LoadLatestBtn.tsx
@@ -1,12 +1,13 @@
 import React from 'react'
 import {StyleSheet, TouchableOpacity, View} from 'react-native'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import Animated from 'react-native-reanimated'
+import {useMediaQuery} from 'react-responsive'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {colors} from 'lib/styles'
 import {HITSLOP_20} from 'lib/constants'
 import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode'
-import Animated from 'react-native-reanimated'
 const AnimatedTouchableOpacity =
   Animated.createAnimatedComponent(TouchableOpacity)
 import {isWeb} from 'platform/detection'
@@ -26,6 +27,9 @@ export function LoadLatestBtn({
   const {isDesktop, isTablet, isMobile, isTabletOrMobile} = useWebMediaQueries()
   const {fabMinimalShellTransform} = useMinimalShellMode()
 
+  // move button inline if it starts overlapping the left nav
+  const isTallViewport = useMediaQuery({minHeight: 700})
+
   // Adjust height of the fab if we have a session only on mobile web. If we don't have a session, we want to adjust
   // it on both tablet and mobile since we are showing the bottom bar (see createNativeStackNavigatorWithAuth)
   const showBottomBar = hasSession ? isMobile : isTabletOrMobile
@@ -34,8 +38,11 @@ export function LoadLatestBtn({
     <AnimatedTouchableOpacity
       style={[
         styles.loadLatest,
-        isDesktop && styles.loadLatestDesktop,
-        isTablet && styles.loadLatestTablet,
+        isDesktop &&
+          (isTallViewport
+            ? styles.loadLatestOutOfLine
+            : styles.loadLatestInline),
+        isTablet && styles.loadLatestInline,
         pal.borderDark,
         pal.view,
         showBottomBar && fabMinimalShellTransform,
@@ -65,11 +72,11 @@ const styles = StyleSheet.create({
     alignItems: 'center',
     justifyContent: 'center',
   },
-  loadLatestTablet: {
+  loadLatestInline: {
     // @ts-ignore web only
     left: 'calc(50vw - 282px)',
   },
-  loadLatestDesktop: {
+  loadLatestOutOfLine: {
     // @ts-ignore web only
     left: 'calc(50vw - 382px)',
   },
diff --git a/src/view/com/util/moderation/ContentHider.tsx b/src/view/com/util/moderation/ContentHider.tsx
deleted file mode 100644
index b3a563116..000000000
--- a/src/view/com/util/moderation/ContentHider.tsx
+++ /dev/null
@@ -1,145 +0,0 @@
-import React from 'react'
-import {Pressable, StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {usePalette} from 'lib/hooks/usePalette'
-import {ModerationUI, PostModeration} from '@atproto/api'
-import {Text} from '../text/Text'
-import {ShieldExclamation} from 'lib/icons'
-import {describeModerationCause} from 'lib/moderation'
-import {useLingui} from '@lingui/react'
-import {msg, Trans} from '@lingui/macro'
-import {useModalControls} from '#/state/modals'
-import {isPostMediaBlurred} from 'lib/moderation'
-
-export function ContentHider({
-  testID,
-  moderation,
-  moderationDecisions,
-  ignoreMute,
-  ignoreQuoteDecisions,
-  style,
-  childContainerStyle,
-  children,
-}: React.PropsWithChildren<{
-  testID?: string
-  moderation: ModerationUI
-  moderationDecisions?: PostModeration['decisions']
-  ignoreMute?: boolean
-  ignoreQuoteDecisions?: boolean
-  style?: StyleProp<ViewStyle>
-  childContainerStyle?: StyleProp<ViewStyle>
-}>) {
-  const pal = usePalette('default')
-  const {_} = useLingui()
-  const [override, setOverride] = React.useState(false)
-  const {openModal} = useModalControls()
-
-  if (
-    !moderation.blur ||
-    (ignoreMute && moderation.cause?.type === 'muted') ||
-    shouldIgnoreQuote(moderationDecisions, ignoreQuoteDecisions)
-  ) {
-    return (
-      <View testID={testID} style={[styles.outer, style]}>
-        {children}
-      </View>
-    )
-  }
-
-  const isMute = moderation.cause?.type === 'muted'
-  const desc = describeModerationCause(moderation.cause, 'content')
-  return (
-    <View testID={testID} style={[styles.outer, style]}>
-      <Pressable
-        onPress={() => {
-          if (!moderation.noOverride) {
-            setOverride(v => !v)
-          } else {
-            openModal({
-              name: 'moderation-details',
-              context: 'content',
-              moderation,
-            })
-          }
-        }}
-        accessibilityRole="button"
-        accessibilityHint={
-          override ? _(msg`Hide the content`) : _(msg`Show the content`)
-        }
-        accessibilityLabel=""
-        style={[
-          styles.cover,
-          moderation.noOverride
-            ? {borderWidth: 1, borderColor: pal.colors.borderDark}
-            : pal.viewLight,
-        ]}>
-        <Pressable
-          onPress={() => {
-            openModal({
-              name: 'moderation-details',
-              context: 'content',
-              moderation,
-            })
-          }}
-          accessibilityRole="button"
-          accessibilityLabel={_(msg`Learn more about this warning`)}
-          accessibilityHint="">
-          {isMute ? (
-            <FontAwesomeIcon
-              icon={['far', 'eye-slash']}
-              size={18}
-              color={pal.colors.textLight}
-            />
-          ) : (
-            <ShieldExclamation size={18} style={pal.textLight} />
-          )}
-        </Pressable>
-        <Text type="md" style={[pal.text, {flex: 1}]} numberOfLines={2}>
-          {desc.name}
-        </Text>
-        <View style={styles.showBtn}>
-          <Text type="lg" style={pal.link}>
-            {moderation.noOverride ? (
-              <Trans>Learn more</Trans>
-            ) : override ? (
-              <Trans>Hide</Trans>
-            ) : (
-              <Trans>Show</Trans>
-            )}
-          </Text>
-        </View>
-      </Pressable>
-      {override && <View style={childContainerStyle}>{children}</View>}
-    </View>
-  )
-}
-
-function shouldIgnoreQuote(
-  decisions: PostModeration['decisions'] | undefined,
-  ignore: boolean | undefined,
-): boolean {
-  if (!decisions || !ignore) {
-    return false
-  }
-  return !isPostMediaBlurred(decisions)
-}
-
-const styles = StyleSheet.create({
-  outer: {
-    overflow: 'hidden',
-  },
-  cover: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    gap: 6,
-    borderRadius: 8,
-    marginTop: 4,
-    paddingVertical: 14,
-    paddingLeft: 14,
-    paddingRight: 18,
-  },
-  showBtn: {
-    marginLeft: 'auto',
-    alignSelf: 'center',
-  },
-})
diff --git a/src/view/com/util/moderation/LabelInfo.tsx b/src/view/com/util/moderation/LabelInfo.tsx
deleted file mode 100644
index 970338752..000000000
--- a/src/view/com/util/moderation/LabelInfo.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-import React from 'react'
-import {Pressable, StyleProp, View, ViewStyle} from 'react-native'
-import {ComAtprotoLabelDefs} from '@atproto/api'
-import {Text} from '../text/Text'
-import {usePalette} from 'lib/hooks/usePalette'
-import {msg, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {useModalControls} from '#/state/modals'
-
-export function LabelInfo({
-  details,
-  labels,
-  style,
-}: {
-  details: {did: string} | {uri: string; cid: string}
-  labels: ComAtprotoLabelDefs.Label[] | undefined
-  style?: StyleProp<ViewStyle>
-}) {
-  const pal = usePalette('default')
-  const {_} = useLingui()
-  const {openModal} = useModalControls()
-
-  if (!labels) {
-    return null
-  }
-  labels = labels.filter(l => !l.val.startsWith('!'))
-  if (!labels.length) {
-    return null
-  }
-
-  return (
-    <View
-      style={[
-        pal.viewLight,
-        {
-          flexDirection: 'row',
-          flexWrap: 'wrap',
-          paddingHorizontal: 12,
-          paddingVertical: 10,
-          borderRadius: 8,
-        },
-        style,
-      ]}>
-      <Text type="sm" style={pal.text}>
-        <Trans>
-          A content warning has been applied to this{' '}
-          {'did' in details ? 'account' : 'post'}.
-        </Trans>{' '}
-      </Text>
-      <Pressable
-        accessibilityRole="button"
-        accessibilityLabel={_(msg`Appeal this decision`)}
-        accessibilityHint=""
-        onPress={() => openModal({name: 'appeal-label', ...details})}>
-        <Text type="sm" style={pal.link}>
-          <Trans>Appeal this decision.</Trans>
-        </Text>
-      </Pressable>
-    </View>
-  )
-}
diff --git a/src/view/com/util/moderation/PostAlerts.tsx b/src/view/com/util/moderation/PostAlerts.tsx
deleted file mode 100644
index bc5bf9b32..000000000
--- a/src/view/com/util/moderation/PostAlerts.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import React from 'react'
-import {Pressable, StyleProp, StyleSheet, ViewStyle} from 'react-native'
-import {ModerationUI} from '@atproto/api'
-import {Text} from '../text/Text'
-import {usePalette} from 'lib/hooks/usePalette'
-import {ShieldExclamation} from 'lib/icons'
-import {describeModerationCause} from 'lib/moderation'
-import {Trans, msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {useModalControls} from '#/state/modals'
-
-export function PostAlerts({
-  moderation,
-  style,
-}: {
-  moderation: ModerationUI
-  includeMute?: boolean
-  style?: StyleProp<ViewStyle>
-}) {
-  const pal = usePalette('default')
-  const {_} = useLingui()
-  const {openModal} = useModalControls()
-
-  const shouldAlert = !!moderation.cause && moderation.alert
-  if (!shouldAlert) {
-    return null
-  }
-
-  const desc = describeModerationCause(moderation.cause, 'content')
-  return (
-    <Pressable
-      onPress={() => {
-        openModal({
-          name: 'moderation-details',
-          context: 'content',
-          moderation,
-        })
-      }}
-      accessibilityRole="button"
-      accessibilityLabel={_(msg`Learn more about this warning`)}
-      accessibilityHint=""
-      style={[styles.container, pal.viewLight, style]}>
-      <ShieldExclamation style={pal.text} size={16} />
-      <Text type="lg" style={[pal.text]}>
-        {desc.name}{' '}
-        <Text type="lg" style={[pal.link, styles.learnMoreBtn]}>
-          <Trans>Learn More</Trans>
-        </Text>
-      </Text>
-    </Pressable>
-  )
-}
-
-const styles = StyleSheet.create({
-  container: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    gap: 4,
-    paddingVertical: 8,
-    paddingLeft: 14,
-    paddingHorizontal: 16,
-    borderRadius: 8,
-  },
-  learnMoreBtn: {
-    marginLeft: 'auto',
-  },
-})
diff --git a/src/view/com/util/moderation/PostHider.tsx b/src/view/com/util/moderation/PostHider.tsx
deleted file mode 100644
index b1fa71d4a..000000000
--- a/src/view/com/util/moderation/PostHider.tsx
+++ /dev/null
@@ -1,142 +0,0 @@
-import React, {ComponentProps} from 'react'
-import {StyleSheet, Pressable, View, ViewStyle, StyleProp} from 'react-native'
-import {ModerationUI} from '@atproto/api'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {usePalette} from 'lib/hooks/usePalette'
-import {Link} from '../Link'
-import {Text} from '../text/Text'
-import {addStyle} from 'lib/styles'
-import {describeModerationCause} from 'lib/moderation'
-import {ShieldExclamation} from 'lib/icons'
-import {useLingui} from '@lingui/react'
-import {Trans, msg} from '@lingui/macro'
-import {useModalControls} from '#/state/modals'
-
-interface Props extends ComponentProps<typeof Link> {
-  iconSize: number
-  iconStyles: StyleProp<ViewStyle>
-  moderation: ModerationUI
-}
-
-export function PostHider({
-  testID,
-  href,
-  moderation,
-  style,
-  children,
-  iconSize,
-  iconStyles,
-  ...props
-}: Props) {
-  const pal = usePalette('default')
-  const {_} = useLingui()
-  const [override, setOverride] = React.useState(false)
-  const {openModal} = useModalControls()
-
-  if (!moderation.blur) {
-    return (
-      <Link
-        testID={testID}
-        style={style}
-        href={href}
-        noFeedback
-        accessible={false}
-        {...props}>
-        {children}
-      </Link>
-    )
-  }
-
-  const isMute = moderation.cause?.type === 'muted'
-  const desc = describeModerationCause(moderation.cause, 'content')
-  return !override ? (
-    <Pressable
-      onPress={() => {
-        if (!moderation.noOverride) {
-          setOverride(v => !v)
-        }
-      }}
-      accessibilityRole="button"
-      accessibilityHint={
-        override ? _(msg`Hide the content`) : _(msg`Show the content`)
-      }
-      accessibilityLabel=""
-      style={[
-        styles.description,
-        override ? {paddingBottom: 0} : undefined,
-        pal.view,
-      ]}>
-      <Pressable
-        onPress={() => {
-          openModal({
-            name: 'moderation-details',
-            context: 'content',
-            moderation,
-          })
-        }}
-        accessibilityRole="button"
-        accessibilityLabel={_(msg`Learn more about this warning`)}
-        accessibilityHint="">
-        <View
-          style={[
-            pal.viewLight,
-            {
-              width: iconSize,
-              height: iconSize,
-              borderRadius: iconSize,
-              alignItems: 'center',
-              justifyContent: 'center',
-            },
-            iconStyles,
-          ]}>
-          {isMute ? (
-            <FontAwesomeIcon
-              icon={['far', 'eye-slash']}
-              size={14}
-              color={pal.colors.textLight}
-            />
-          ) : (
-            <ShieldExclamation size={14} style={pal.textLight} />
-          )}
-        </View>
-      </Pressable>
-      <Text type="sm" style={[{flex: 1}, pal.textLight]} numberOfLines={1}>
-        {desc.name}
-      </Text>
-      {!moderation.noOverride && (
-        <Text type="sm" style={[styles.showBtn, pal.link]}>
-          {override ? <Trans>Hide</Trans> : <Trans>Show</Trans>}
-        </Text>
-      )}
-    </Pressable>
-  ) : (
-    <Link
-      testID={testID}
-      style={addStyle(style, styles.child)}
-      href={href}
-      noFeedback>
-      {children}
-    </Link>
-  )
-}
-
-const styles = StyleSheet.create({
-  description: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    gap: 4,
-    paddingVertical: 10,
-    paddingLeft: 6,
-    paddingRight: 18,
-    marginTop: 1,
-  },
-  showBtn: {
-    marginLeft: 'auto',
-    alignSelf: 'center',
-  },
-  child: {
-    borderWidth: 0,
-    borderTopWidth: 0,
-    borderRadius: 8,
-  },
-})
diff --git a/src/view/com/util/moderation/ProfileHeaderAlerts.tsx b/src/view/com/util/moderation/ProfileHeaderAlerts.tsx
deleted file mode 100644
index 0f07b679b..000000000
--- a/src/view/com/util/moderation/ProfileHeaderAlerts.tsx
+++ /dev/null
@@ -1,89 +0,0 @@
-import React from 'react'
-import {Pressable, StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
-import {ProfileModeration} from '@atproto/api'
-import {Text} from '../text/Text'
-import {usePalette} from 'lib/hooks/usePalette'
-import {ShieldExclamation} from 'lib/icons'
-import {
-  describeModerationCause,
-  getProfileModerationCauses,
-} from 'lib/moderation'
-import {msg, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {useModalControls} from '#/state/modals'
-
-export function ProfileHeaderAlerts({
-  moderation,
-  style,
-}: {
-  moderation: ProfileModeration
-  style?: StyleProp<ViewStyle>
-}) {
-  const pal = usePalette('default')
-  const {_} = useLingui()
-  const {openModal} = useModalControls()
-
-  const causes = getProfileModerationCauses(moderation)
-  if (!causes.length) {
-    return null
-  }
-
-  return (
-    <View style={styles.grid}>
-      {causes.map(cause => {
-        const isMute = cause.type === 'muted'
-        const desc = describeModerationCause(cause, 'account')
-        return (
-          <Pressable
-            testID="profileHeaderAlert"
-            key={desc.name}
-            onPress={() => {
-              openModal({
-                name: 'moderation-details',
-                context: 'content',
-                moderation: {cause},
-              })
-            }}
-            accessibilityRole="button"
-            accessibilityLabel={_(msg`Learn more about this warning`)}
-            accessibilityHint=""
-            style={[styles.container, pal.viewLight, style]}>
-            {isMute ? (
-              <FontAwesomeIcon
-                icon={['far', 'eye-slash']}
-                size={14}
-                color={pal.colors.textLight}
-              />
-            ) : (
-              <ShieldExclamation style={pal.text} size={18} />
-            )}
-            <Text type="sm" style={[{flex: 1}, pal.text]}>
-              {desc.name}
-            </Text>
-            <Text type="sm" style={[pal.link, styles.learnMoreBtn]}>
-              <Trans>Learn More</Trans>
-            </Text>
-          </Pressable>
-        )
-      })}
-    </View>
-  )
-}
-
-const styles = StyleSheet.create({
-  grid: {
-    gap: 4,
-  },
-  container: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    gap: 8,
-    paddingVertical: 12,
-    paddingHorizontal: 16,
-    borderRadius: 8,
-  },
-  learnMoreBtn: {
-    marginLeft: 'auto',
-  },
-})
diff --git a/src/view/com/util/moderation/ScreenHider.tsx b/src/view/com/util/moderation/ScreenHider.tsx
deleted file mode 100644
index 86f0cbf7b..000000000
--- a/src/view/com/util/moderation/ScreenHider.tsx
+++ /dev/null
@@ -1,180 +0,0 @@
-import React from 'react'
-import {
-  TouchableWithoutFeedback,
-  StyleProp,
-  StyleSheet,
-  View,
-  ViewStyle,
-} from 'react-native'
-import {
-  FontAwesomeIcon,
-  FontAwesomeIconStyle,
-} from '@fortawesome/react-native-fontawesome'
-import {useNavigation} from '@react-navigation/native'
-import {ModerationUI} from '@atproto/api'
-import {usePalette} from 'lib/hooks/usePalette'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {NavigationProp} from 'lib/routes/types'
-import {Text} from '../text/Text'
-import {Button} from '../forms/Button'
-import {describeModerationCause} from 'lib/moderation'
-import {Trans, msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {useModalControls} from '#/state/modals'
-import {s} from '#/lib/styles'
-import {CenteredView} from '../Views'
-
-export function ScreenHider({
-  testID,
-  screenDescription,
-  moderation,
-  style,
-  containerStyle,
-  children,
-}: React.PropsWithChildren<{
-  testID?: string
-  screenDescription: string
-  moderation: ModerationUI
-  style?: StyleProp<ViewStyle>
-  containerStyle?: StyleProp<ViewStyle>
-}>) {
-  const pal = usePalette('default')
-  const palInverted = usePalette('inverted')
-  const {_} = useLingui()
-  const [override, setOverride] = React.useState(false)
-  const navigation = useNavigation<NavigationProp>()
-  const {isMobile} = useWebMediaQueries()
-  const {openModal} = useModalControls()
-
-  if (!moderation.blur || override) {
-    return (
-      <View testID={testID} style={style}>
-        {children}
-      </View>
-    )
-  }
-
-  const isNoPwi =
-    moderation.cause?.type === 'label' &&
-    moderation.cause?.labelDef.id === '!no-unauthenticated'
-  const desc = describeModerationCause(moderation.cause, 'account')
-  return (
-    <CenteredView
-      style={[styles.container, pal.view, containerStyle]}
-      sideBorders>
-      <View style={styles.iconContainer}>
-        <View style={[styles.icon, palInverted.view]}>
-          <FontAwesomeIcon
-            icon={isNoPwi ? ['far', 'eye-slash'] : 'exclamation'}
-            style={pal.textInverted as FontAwesomeIconStyle}
-            size={24}
-          />
-        </View>
-      </View>
-      <Text type="title-2xl" style={[styles.title, pal.text]}>
-        {isNoPwi ? (
-          <Trans>Sign-in Required</Trans>
-        ) : (
-          <Trans>Content Warning</Trans>
-        )}
-      </Text>
-      <Text type="2xl" style={[styles.description, pal.textLight]}>
-        {isNoPwi ? (
-          <Trans>
-            This account has requested that users sign in to view their profile.
-          </Trans>
-        ) : (
-          <>
-            <Trans>This {screenDescription} has been flagged:</Trans>
-            <Text type="2xl-medium" style={[pal.text, s.ml5]}>
-              {desc.name}.
-            </Text>
-            <TouchableWithoutFeedback
-              onPress={() => {
-                openModal({
-                  name: 'moderation-details',
-                  context: 'account',
-                  moderation,
-                })
-              }}
-              accessibilityRole="button"
-              accessibilityLabel={_(msg`Learn more about this warning`)}
-              accessibilityHint="">
-              <Text type="2xl" style={pal.link}>
-                <Trans>Learn More</Trans>
-              </Text>
-            </TouchableWithoutFeedback>
-          </>
-        )}{' '}
-      </Text>
-      {isMobile && <View style={styles.spacer} />}
-      <View style={styles.btnContainer}>
-        <Button
-          type="inverted"
-          onPress={() => {
-            if (navigation.canGoBack()) {
-              navigation.goBack()
-            } else {
-              navigation.navigate('Home')
-            }
-          }}
-          style={styles.btn}>
-          <Text type="button-lg" style={pal.textInverted}>
-            <Trans>Go back</Trans>
-          </Text>
-        </Button>
-        {!moderation.noOverride && (
-          <Button
-            type="default"
-            onPress={() => setOverride(v => !v)}
-            style={styles.btn}>
-            <Text type="button-lg" style={pal.text}>
-              <Trans>Show anyway</Trans>
-            </Text>
-          </Button>
-        )}
-      </View>
-    </CenteredView>
-  )
-}
-
-const styles = StyleSheet.create({
-  spacer: {
-    flex: 1,
-  },
-  container: {
-    flex: 1,
-    paddingTop: 100,
-    paddingBottom: 150,
-  },
-  iconContainer: {
-    alignItems: 'center',
-    marginBottom: 10,
-  },
-  icon: {
-    borderRadius: 25,
-    width: 50,
-    height: 50,
-    alignItems: 'center',
-    justifyContent: 'center',
-  },
-  title: {
-    textAlign: 'center',
-    marginBottom: 10,
-  },
-  description: {
-    marginBottom: 10,
-    paddingHorizontal: 20,
-    textAlign: 'center',
-  },
-  btnContainer: {
-    flexDirection: 'row',
-    justifyContent: 'center',
-    marginVertical: 10,
-    gap: 10,
-  },
-  btn: {
-    paddingHorizontal: 20,
-    paddingVertical: 14,
-  },
-})
diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx
index bd21ddda2..58874cd55 100644
--- a/src/view/com/util/post-ctrls/PostCtrls.tsx
+++ b/src/view/com/util/post-ctrls/PostCtrls.tsx
@@ -12,54 +12,67 @@ import {
   AtUri,
   RichText as RichTextAPI,
 } from '@atproto/api'
-import {Text} from '../text/Text'
-import {PostDropdownBtn} from '../forms/PostDropdownBtn'
-import {HeartIcon, HeartIconSolid, CommentBottomArrow} from 'lib/icons'
-import {s} from 'lib/styles'
-import {pluralize} from 'lib/strings/helpers'
-import {useTheme} from 'lib/ThemeContext'
-import {RepostButton} from './RepostButton'
-import {Haptics} from 'lib/haptics'
-import {HITSLOP_10, HITSLOP_20} from 'lib/constants'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {HITSLOP_10, HITSLOP_20} from '#/lib/constants'
+import {Haptics} from '#/lib/haptics'
+import {CommentBottomArrow, HeartIcon, HeartIconSolid} from '#/lib/icons'
+import {makeProfileLink} from '#/lib/routes/links'
+import {shareUrl} from '#/lib/sharing'
+import {pluralize} from '#/lib/strings/helpers'
+import {toShareUrl} from '#/lib/strings/url-helpers'
+import {s} from '#/lib/styles'
+import {useTheme} from '#/lib/ThemeContext'
+import {Shadow} from '#/state/cache/types'
 import {useModalControls} from '#/state/modals'
 import {
   usePostLikeMutationQueue,
   usePostRepostMutationQueue,
 } from '#/state/queries/post'
-import {useComposerControls} from '#/state/shell/composer'
-import {Shadow} from '#/state/cache/types'
 import {useRequireAuth} from '#/state/session'
-import {msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
+import {useComposerControls} from '#/state/shell/composer'
+import {useDialogControl} from '#/components/Dialog'
 import {ArrowOutOfBox_Stroke2_Corner0_Rounded as ArrowOutOfBox} from '#/components/icons/ArrowOutOfBox'
-import {toShareUrl} from 'lib/strings/url-helpers'
-import {shareUrl} from 'lib/sharing'
-import {makeProfileLink} from 'lib/routes/links'
+import * as Prompt from '#/components/Prompt'
+import {PostDropdownBtn} from '../forms/PostDropdownBtn'
+import {Text} from '../text/Text'
+import {RepostButton} from './RepostButton'
 
 let PostCtrls = ({
   big,
   post,
   record,
   richText,
-  showAppealLabelItem,
   style,
   onPressReply,
+  logContext,
 }: {
   big?: boolean
   post: Shadow<AppBskyFeedDefs.PostView>
   record: AppBskyFeedPost.Record
   richText: RichTextAPI
-  showAppealLabelItem?: boolean
   style?: StyleProp<ViewStyle>
   onPressReply: () => void
+  logContext: 'FeedItem' | 'PostThreadItem' | 'Post'
 }): React.ReactNode => {
   const theme = useTheme()
   const {_} = useLingui()
   const {openComposer} = useComposerControls()
   const {closeModal} = useModalControls()
-  const [queueLike, queueUnlike] = usePostLikeMutationQueue(post)
-  const [queueRepost, queueUnrepost] = usePostRepostMutationQueue(post)
+  const [queueLike, queueUnlike] = usePostLikeMutationQueue(post, logContext)
+  const [queueRepost, queueUnrepost] = usePostRepostMutationQueue(
+    post,
+    logContext,
+  )
   const requireAuth = useRequireAuth()
+  const loggedOutWarningPromptControl = useDialogControl()
+
+  const shouldShowLoggedOutWarning = React.useMemo(() => {
+    return !!post.author.labels?.find(
+      label => label.val === '!no-unauthenticated',
+    )
+  }, [post])
 
   const defaultCtrlColor = React.useMemo(
     () => ({
@@ -206,20 +219,38 @@ let PostCtrls = ({
         </TouchableOpacity>
       </View>
       {big && (
-        <View style={styles.ctrlBig}>
-          <TouchableOpacity
-            testID="shareBtn"
-            style={[styles.btn]}
-            onPress={onShare}
-            accessibilityRole="button"
-            accessibilityLabel={`${
-              post.viewer?.like ? _(msg`Unlike`) : _(msg`Like`)
-            } (${post.likeCount} ${pluralize(post.likeCount || 0, 'like')})`}
-            accessibilityHint=""
-            hitSlop={big ? HITSLOP_20 : HITSLOP_10}>
-            <ArrowOutOfBox style={[defaultCtrlColor, styles.mt1]} width={22} />
-          </TouchableOpacity>
-        </View>
+        <>
+          <View style={styles.ctrlBig}>
+            <TouchableOpacity
+              testID="shareBtn"
+              style={[styles.btn]}
+              onPress={() => {
+                if (shouldShowLoggedOutWarning) {
+                  loggedOutWarningPromptControl.open()
+                } else {
+                  onShare()
+                }
+              }}
+              accessibilityRole="button"
+              accessibilityLabel={`${_(msg`Share`)}`}
+              accessibilityHint=""
+              hitSlop={big ? HITSLOP_20 : HITSLOP_10}>
+              <ArrowOutOfBox
+                style={[defaultCtrlColor, styles.mt1]}
+                width={22}
+              />
+            </TouchableOpacity>
+          </View>
+          <Prompt.Basic
+            control={loggedOutWarningPromptControl}
+            title={_(msg`Note about sharing`)}
+            description={_(
+              msg`This post is only visible to logged-in users. It won't be visible to people who aren't logged in.`,
+            )}
+            onConfirm={onShare}
+            confirmButtonCta={_(msg`Share anyway`)}
+          />
+        </>
       )}
       <View style={big ? styles.ctrlBig : styles.ctrl}>
         <PostDropdownBtn
@@ -229,8 +260,8 @@ let PostCtrls = ({
           postUri={post.uri}
           record={record}
           richText={richText}
-          showAppealLabelItem={showAppealLabelItem}
           style={styles.btnPad}
+          hitSlop={big ? HITSLOP_20 : HITSLOP_10}
         />
       </View>
     </View>
diff --git a/src/view/com/util/post-embeds/ExternalPlayerEmbed.tsx b/src/view/com/util/post-embeds/ExternalPlayerEmbed.tsx
index d556e7669..cf2db5b33 100644
--- a/src/view/com/util/post-embeds/ExternalPlayerEmbed.tsx
+++ b/src/view/com/util/post-embeds/ExternalPlayerEmbed.tsx
@@ -21,7 +21,7 @@ import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useNavigation} from '@react-navigation/native'
 import {AppBskyEmbedExternal} from '@atproto/api'
-import {EmbedPlayerParams, getPlayerHeight} from 'lib/strings/embed-player'
+import {EmbedPlayerParams, getPlayerAspect} from 'lib/strings/embed-player'
 import {EventStopper} from '../EventStopper'
 import {isNative} from 'platform/detection'
 import {NavigationProp} from 'lib/routes/types'
@@ -67,14 +67,12 @@ function PlaceholderOverlay({
 
 // This renders the webview/youtube player as a separate layer
 function Player({
-  height,
   params,
   onLoad,
   isPlayerActive,
 }: {
   isPlayerActive: boolean
   params: EmbedPlayerParams
-  height: number
   onLoad: () => void
 }) {
   // ensures we only load what's requested
@@ -91,25 +89,21 @@ function Player({
   if (!isPlayerActive) return null
 
   return (
-    <View style={[styles.layer, styles.playerLayer]}>
-      <EventStopper>
-        <View style={{height, width: '100%'}}>
-          <WebView
-            javaScriptEnabled={true}
-            onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
-            mediaPlaybackRequiresUserAction={false}
-            allowsInlineMediaPlayback
-            bounces={false}
-            allowsFullscreenVideo
-            nestedScrollEnabled
-            source={{uri: params.playerUri}}
-            onLoad={onLoad}
-            setSupportMultipleWindows={false} // Prevent any redirects from opening a new window (ads)
-            style={[styles.webview, styles.topRadius]}
-          />
-        </View>
-      </EventStopper>
-    </View>
+    <EventStopper style={[styles.layer, styles.playerLayer]}>
+      <WebView
+        javaScriptEnabled={true}
+        onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
+        mediaPlaybackRequiresUserAction={false}
+        allowsInlineMediaPlayback
+        bounces={false}
+        allowsFullscreenVideo
+        nestedScrollEnabled
+        source={{uri: params.playerUri}}
+        onLoad={onLoad}
+        style={styles.webview}
+        setSupportMultipleWindows={false} // Prevent any redirects from opening a new window (ads)
+      />
+    </EventStopper>
   )
 }
 
@@ -129,13 +123,16 @@ export function ExternalPlayer({
 
   const [isPlayerActive, setPlayerActive] = React.useState(false)
   const [isLoading, setIsLoading] = React.useState(true)
-  const [dim, setDim] = React.useState({
-    width: 0,
-    height: 0,
-  })
 
-  const viewRef = useAnimatedRef()
+  const aspect = React.useMemo(() => {
+    return getPlayerAspect({
+      type: params.type,
+      width: windowDims.width,
+      hasThumb: !!link.thumb,
+    })
+  }, [params.type, windowDims.width, link.thumb])
 
+  const viewRef = useAnimatedRef()
   const frameCallback = useFrameCallback(() => {
     const measurement = measure(viewRef)
     if (!measurement) return
@@ -180,17 +177,6 @@ export function ExternalPlayer({
     }
   }, [navigation, isPlayerActive, frameCallback])
 
-  // calculate height for the player and the screen size
-  const height = React.useMemo(
-    () =>
-      getPlayerHeight({
-        type: params.type,
-        width: dim.width,
-        hasThumb: !!link.thumb,
-      }),
-    [params.type, dim.width, link.thumb],
-  )
-
   const onLoad = React.useCallback(() => {
     setIsLoading(false)
   }, [])
@@ -216,32 +202,11 @@ export function ExternalPlayer({
     [externalEmbedsPrefs, openModal, params.source],
   )
 
-  // measure the layout to set sizing
-  const onLayout = React.useCallback(
-    (event: {nativeEvent: {layout: {width: any; height: any}}}) => {
-      setDim({
-        width: event.nativeEvent.layout.width,
-        height: event.nativeEvent.layout.height,
-      })
-    },
-    [],
-  )
-
   return (
-    <Animated.View
-      ref={viewRef}
-      style={{height}}
-      collapsable={false}
-      onLayout={onLayout}>
+    <Animated.View ref={viewRef} collapsable={false} style={[aspect]}>
       {link.thumb && (!isPlayerActive || isLoading) && (
         <Image
-          style={[
-            {
-              width: dim.width,
-              height,
-            },
-            styles.topRadius,
-          ]}
+          style={[{flex: 1}, styles.topRadius]}
           source={{uri: link.thumb}}
           accessibilityIgnoresInvertColors
         />
@@ -251,12 +216,7 @@ export function ExternalPlayer({
         isPlayerActive={isPlayerActive}
         onPress={onPlayPress}
       />
-      <Player
-        isPlayerActive={isPlayerActive}
-        params={params}
-        height={height}
-        onLoad={onLoad}
-      />
+      <Player isPlayerActive={isPlayerActive} params={params} onLoad={onLoad} />
     </Animated.View>
   )
 }
diff --git a/src/view/com/util/post-embeds/QuoteEmbed.tsx b/src/view/com/util/post-embeds/QuoteEmbed.tsx
index d9d84feb4..2b1c3e617 100644
--- a/src/view/com/util/post-embeds/QuoteEmbed.tsx
+++ b/src/view/com/util/post-embeds/QuoteEmbed.tsx
@@ -1,13 +1,15 @@
 import React from 'react'
 import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
 import {
+  AppBskyFeedDefs,
   AppBskyEmbedRecord,
   AppBskyFeedPost,
   AppBskyEmbedImages,
   AppBskyEmbedRecordWithMedia,
-  ModerationUI,
   AppBskyEmbedExternal,
   RichText as RichTextAPI,
+  moderatePost,
+  ModerationDecision,
 } from '@atproto/api'
 import {AtUri} from '@atproto/api'
 import {PostMeta} from '../PostMeta'
@@ -16,19 +18,20 @@ import {Text} from '../text/Text'
 import {usePalette} from 'lib/hooks/usePalette'
 import {ComposerOptsQuote} from 'state/shell/composer'
 import {PostEmbeds} from '.'
-import {PostAlerts} from '../moderation/PostAlerts'
+import {PostAlerts} from '../../../../components/moderation/PostAlerts'
 import {makeProfileLink} from 'lib/routes/links'
 import {InfoCircleIcon} from 'lib/icons'
 import {Trans} from '@lingui/macro'
-import {RichText} from 'view/com/util/text/RichText'
+import {useModerationOpts} from '#/state/queries/preferences'
+import {ContentHider} from '../../../../components/moderation/ContentHider'
+import {RichText} from '#/components/RichText'
+import {atoms as a} from '#/alf'
 
 export function MaybeQuoteEmbed({
   embed,
-  moderation,
   style,
 }: {
   embed: AppBskyEmbedRecord.View
-  moderation: ModerationUI
   style?: StyleProp<ViewStyle>
 }) {
   const pal = usePalette('default')
@@ -38,17 +41,9 @@ export function MaybeQuoteEmbed({
     AppBskyFeedPost.validateRecord(embed.record.value).success
   ) {
     return (
-      <QuoteEmbed
-        quote={{
-          author: embed.record.author,
-          cid: embed.record.cid,
-          uri: embed.record.uri,
-          indexedAt: embed.record.indexedAt,
-          text: embed.record.value.text,
-          facets: embed.record.value.facets,
-          embeds: embed.record.embeds,
-        }}
-        moderation={moderation}
+      <QuoteEmbedModerated
+        viewRecord={embed.record}
+        postRecord={embed.record.value}
         style={style}
       />
     )
@@ -74,19 +69,49 @@ export function MaybeQuoteEmbed({
   return null
 }
 
+function QuoteEmbedModerated({
+  viewRecord,
+  postRecord,
+  style,
+}: {
+  viewRecord: AppBskyEmbedRecord.ViewRecord
+  postRecord: AppBskyFeedPost.Record
+  style?: StyleProp<ViewStyle>
+}) {
+  const moderationOpts = useModerationOpts()
+  const moderation = React.useMemo(() => {
+    return moderationOpts
+      ? moderatePost(viewRecordToPostView(viewRecord), moderationOpts)
+      : undefined
+  }, [viewRecord, moderationOpts])
+
+  const quote = {
+    author: viewRecord.author,
+    cid: viewRecord.cid,
+    uri: viewRecord.uri,
+    indexedAt: viewRecord.indexedAt,
+    text: postRecord.text,
+    facets: postRecord.facets,
+    embeds: viewRecord.embeds,
+  }
+
+  return <QuoteEmbed quote={quote} moderation={moderation} style={style} />
+}
+
 export function QuoteEmbed({
   quote,
   moderation,
   style,
 }: {
   quote: ComposerOptsQuote
-  moderation?: ModerationUI
+  moderation?: ModerationDecision
   style?: StyleProp<ViewStyle>
 }) {
   const pal = usePalette('default')
   const itemUrip = new AtUri(quote.uri)
   const itemHref = makeProfileLink(quote.author, 'post', itemUrip.rkey)
   const itemTitle = `Post by ${quote.author.handle}`
+
   const richText = React.useMemo(
     () =>
       quote.text.trim()
@@ -94,6 +119,7 @@ export function QuoteEmbed({
         : undefined,
     [quote.text, quote.facets],
   )
+
   const embed = React.useMemo(() => {
     const e = quote.embeds?.[0]
 
@@ -107,39 +133,52 @@ export function QuoteEmbed({
       return e.media
     }
   }, [quote.embeds])
+
   return (
-    <Link
-      style={[styles.container, pal.borderDark, style]}
-      hoverStyle={{borderColor: pal.colors.borderLinkHover}}
-      href={itemHref}
-      title={itemTitle}>
-      <View pointerEvents="none">
-        <PostMeta
-          author={quote.author}
-          showAvatar
-          authorHasWarning={false}
-          postHref={itemHref}
-          timestamp={quote.indexedAt}
-        />
-      </View>
-      {moderation ? (
-        <PostAlerts moderation={moderation} style={styles.alert} />
-      ) : null}
-      {richText ? (
-        <RichText
-          richText={richText}
-          type="post-text"
-          style={pal.text}
-          numberOfLines={20}
-          noLinks
-        />
-      ) : null}
-      {embed && <PostEmbeds embed={embed} moderation={{}} />}
-    </Link>
+    <ContentHider modui={moderation?.ui('contentList')}>
+      <Link
+        style={[styles.container, pal.borderDark, style]}
+        hoverStyle={{borderColor: pal.colors.borderLinkHover}}
+        href={itemHref}
+        title={itemTitle}>
+        <View pointerEvents="none">
+          <PostMeta
+            author={quote.author}
+            moderation={moderation}
+            showAvatar
+            authorHasWarning={false}
+            postHref={itemHref}
+            timestamp={quote.indexedAt}
+          />
+        </View>
+        {moderation ? (
+          <PostAlerts modui={moderation.ui('contentView')} style={[a.py_xs]} />
+        ) : null}
+        {richText ? (
+          <RichText
+            value={richText}
+            style={[a.text_md]}
+            numberOfLines={20}
+            disableLinks
+          />
+        ) : null}
+        {embed && <PostEmbeds embed={embed} moderation={moderation} />}
+      </Link>
+    </ContentHider>
   )
 }
 
-export default QuoteEmbed
+function viewRecordToPostView(
+  viewRecord: AppBskyEmbedRecord.ViewRecord,
+): AppBskyFeedDefs.PostView {
+  const {value, embeds, ...rest} = viewRecord
+  return {
+    ...rest,
+    $type: 'app.bsky.feed.defs#postView',
+    record: value,
+    embed: embeds?.[0],
+  }
+}
 
 const styles = StyleSheet.create({
   container: {
diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx
index 7e235babb..47091fbb0 100644
--- a/src/view/com/util/post-embeds/index.tsx
+++ b/src/view/com/util/post-embeds/index.tsx
@@ -15,8 +15,7 @@ import {
   AppBskyEmbedRecordWithMedia,
   AppBskyFeedDefs,
   AppBskyGraphDefs,
-  ModerationUI,
-  PostModeration,
+  ModerationDecision,
 } from '@atproto/api'
 import {Link} from '../Link'
 import {ImageLayoutGrid} from '../images/ImageLayoutGrid'
@@ -26,9 +25,8 @@ import {ExternalLinkEmbed} from './ExternalLinkEmbed'
 import {MaybeQuoteEmbed} from './QuoteEmbed'
 import {AutoSizedImage} from '../images/AutoSizedImage'
 import {ListEmbed} from './ListEmbed'
-import {isCauseALabelOnUri, isQuoteBlurred} from 'lib/moderation'
 import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard'
-import {ContentHider} from '../moderation/ContentHider'
+import {ContentHider} from '../../../../components/moderation/ContentHider'
 import {isNative} from '#/platform/detection'
 import {shareUrl} from '#/lib/sharing'
 
@@ -42,12 +40,10 @@ type Embed =
 export function PostEmbeds({
   embed,
   moderation,
-  moderationDecisions,
   style,
 }: {
   embed?: Embed
-  moderation: ModerationUI
-  moderationDecisions?: PostModeration['decisions']
+  moderation?: ModerationDecision
   style?: StyleProp<ViewStyle>
 }) {
   const pal = usePalette('default')
@@ -66,18 +62,10 @@ export function PostEmbeds({
   // quote post with media
   // =
   if (AppBskyEmbedRecordWithMedia.isView(embed)) {
-    const isModOnQuote =
-      (AppBskyEmbedRecord.isViewRecord(embed.record.record) &&
-        isCauseALabelOnUri(moderation.cause, embed.record.record.uri)) ||
-      (moderationDecisions && isQuoteBlurred(moderationDecisions))
-    const mediaModeration = isModOnQuote ? {} : moderation
-    const quoteModeration = isModOnQuote ? moderation : {}
     return (
       <View style={style}>
-        <PostEmbeds embed={embed.media} moderation={mediaModeration} />
-        <ContentHider moderation={quoteModeration}>
-          <MaybeQuoteEmbed embed={embed.record} moderation={quoteModeration} />
-        </ContentHider>
+        <PostEmbeds embed={embed.media} moderation={moderation} />
+        <MaybeQuoteEmbed embed={embed.record} />
       </View>
     )
   }
@@ -86,6 +74,7 @@ export function PostEmbeds({
     // custom feed embed (i.e. generator view)
     // =
     if (AppBskyFeedDefs.isGeneratorView(embed.record)) {
+      // TODO moderation
       return (
         <FeedSourceCard
           feedUri={embed.record.uri}
@@ -97,16 +86,13 @@ export function PostEmbeds({
 
     // list embed
     if (AppBskyGraphDefs.isListView(embed.record)) {
+      // TODO moderation
       return <ListEmbed item={embed.record} />
     }
 
     // quote post
     // =
-    return (
-      <ContentHider moderation={moderation}>
-        <MaybeQuoteEmbed embed={embed} style={style} moderation={moderation} />
-      </ContentHider>
-    )
+    return <MaybeQuoteEmbed embed={embed} style={style} />
   }
 
   // image embed
@@ -132,35 +118,41 @@ export function PostEmbeds({
       if (images.length === 1) {
         const {alt, thumb, aspectRatio} = images[0]
         return (
-          <View style={[styles.imagesContainer, style]}>
-            <AutoSizedImage
-              alt={alt}
-              uri={thumb}
-              dimensionsHint={aspectRatio}
-              onPress={() => _openLightbox(0)}
-              onPressIn={() => onPressIn(0)}
-              style={[styles.singleImage]}>
-              {alt === '' ? null : (
-                <View style={styles.altContainer}>
-                  <Text style={styles.alt} accessible={false}>
-                    ALT
-                  </Text>
-                </View>
-              )}
-            </AutoSizedImage>
-          </View>
+          <ContentHider modui={moderation?.ui('contentMedia')}>
+            <View style={[styles.imagesContainer, style]}>
+              <AutoSizedImage
+                alt={alt}
+                uri={thumb}
+                dimensionsHint={aspectRatio}
+                onPress={() => _openLightbox(0)}
+                onPressIn={() => onPressIn(0)}
+                style={[styles.singleImage]}>
+                {alt === '' ? null : (
+                  <View style={styles.altContainer}>
+                    <Text style={styles.alt} accessible={false}>
+                      ALT
+                    </Text>
+                  </View>
+                )}
+              </AutoSizedImage>
+            </View>
+          </ContentHider>
         )
       }
 
       return (
-        <View style={[styles.imagesContainer, style]}>
-          <ImageLayoutGrid
-            images={embed.images}
-            onPress={_openLightbox}
-            onPressIn={onPressIn}
-            style={embed.images.length === 1 ? [styles.singleImage] : undefined}
-          />
-        </View>
+        <ContentHider modui={moderation?.ui('contentMedia')}>
+          <View style={[styles.imagesContainer, style]}>
+            <ImageLayoutGrid
+              images={embed.images}
+              onPress={_openLightbox}
+              onPressIn={onPressIn}
+              style={
+                embed.images.length === 1 ? [styles.singleImage] : undefined
+              }
+            />
+          </View>
+        </ContentHider>
       )
     }
   }
@@ -171,15 +163,17 @@ export function PostEmbeds({
     const link = embed.external
 
     return (
-      <Link
-        asAnchor
-        anchorNoUnderline
-        href={link.uri}
-        style={[styles.extOuter, pal.view, pal.borderDark, style]}
-        hoverStyle={{borderColor: pal.colors.borderLinkHover}}
-        onLongPress={onShareExternal}>
-        <ExternalLinkEmbed link={link} />
-      </Link>
+      <ContentHider modui={moderation?.ui('contentMedia')}>
+        <Link
+          asAnchor
+          anchorNoUnderline
+          href={link.uri}
+          style={[styles.extOuter, pal.view, pal.borderDark, style]}
+          hoverStyle={{borderColor: pal.colors.borderLinkHover}}
+          onLongPress={onShareExternal}>
+          <ExternalLinkEmbed link={link} />
+        </Link>
+      </ContentHider>
     )
   }
 
diff --git a/src/view/com/util/text/RichText.tsx b/src/view/com/util/text/RichText.tsx
index e910127fe..f4ade30e5 100644
--- a/src/view/com/util/text/RichText.tsx
+++ b/src/view/com/util/text/RichText.tsx
@@ -7,9 +7,15 @@ import {lh} from 'lib/styles'
 import {toShortUrl} from 'lib/strings/url-helpers'
 import {useTheme, TypographyVariant} from 'lib/ThemeContext'
 import {usePalette} from 'lib/hooks/usePalette'
+import {makeTagLink} from 'lib/routes/links'
+import {TagMenu, useTagMenuControl} from '#/components/TagMenu'
+import {isNative} from '#/platform/detection'
 
 const WORD_WRAP = {wordWrap: 1}
 
+/**
+ * @deprecated use `#/components/RichText`
+ */
 export function RichText({
   testID,
   type = 'md',
@@ -79,6 +85,7 @@ export function RichText({
   for (const segment of richText.segments()) {
     const link = segment.link
     const mention = segment.mention
+    const tag = segment.tag
     if (
       !noLinks &&
       mention &&
@@ -107,11 +114,25 @@ export function RichText({
             href={link.uri}
             style={[style, lineHeightStyle, pal.link, {pointerEvents: 'auto'}]}
             dataSet={WORD_WRAP}
-            warnOnMismatchingLabel
             selectable={selectable}
           />,
         )
       }
+    } else if (
+      !noLinks &&
+      tag &&
+      AppBskyRichtextFacet.validateTag(tag).success
+    ) {
+      els.push(
+        <RichTextTag
+          key={key}
+          text={segment.text}
+          type={type}
+          style={style}
+          lineHeightStyle={lineHeightStyle}
+          selectable={selectable}
+        />,
+      )
     } else {
       els.push(segment.text)
     }
@@ -130,3 +151,50 @@ export function RichText({
     </Text>
   )
 }
+
+function RichTextTag({
+  text: tag,
+  type,
+  style,
+  lineHeightStyle,
+  selectable,
+}: {
+  text: string
+  type?: TypographyVariant
+  style?: StyleProp<TextStyle>
+  lineHeightStyle?: TextStyle
+  selectable?: boolean
+}) {
+  const pal = usePalette('default')
+  const control = useTagMenuControl()
+
+  const open = React.useCallback(() => {
+    control.open()
+  }, [control])
+
+  return (
+    <React.Fragment>
+      <TagMenu control={control} tag={tag}>
+        {isNative ? (
+          <TextLink
+            type={type}
+            text={tag}
+            // segment.text has the leading "#" while tag.tag does not
+            href={makeTagLink(tag)}
+            style={[style, lineHeightStyle, pal.link, {pointerEvents: 'auto'}]}
+            dataSet={WORD_WRAP}
+            selectable={selectable}
+            onPress={open}
+          />
+        ) : (
+          <Text
+            selectable={selectable}
+            type={type}
+            style={[style, lineHeightStyle, pal.link, {pointerEvents: 'auto'}]}>
+            {tag}
+          </Text>
+        )}
+      </TagMenu>
+    </React.Fragment>
+  )
+}
diff --git a/src/view/com/util/text/Text.tsx b/src/view/com/util/text/Text.tsx
index ccb51bfca..37d665581 100644
--- a/src/view/com/util/text/Text.tsx
+++ b/src/view/com/util/text/Text.tsx
@@ -2,7 +2,7 @@ import React from 'react'
 import {Text as RNText, TextProps} from 'react-native'
 import {s, lh} from 'lib/styles'
 import {useTheme, TypographyVariant} from 'lib/ThemeContext'
-import {isIOS} from 'platform/detection'
+import {isIOS, isWeb} from 'platform/detection'
 import {UITextView} from 'react-native-ui-text-view'
 
 export type CustomTextProps = TextProps & {
@@ -13,6 +13,11 @@ export type CustomTextProps = TextProps & {
   selectable?: boolean
 }
 
+const fontFamilyStyle = {
+  fontFamily:
+    '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Liberation Sans", Helvetica, Arial, sans-serif',
+}
+
 export function Text({
   type = 'md',
   children,
@@ -39,7 +44,13 @@ export function Text({
 
   return (
     <RNText
-      style={[s.black, typography, lineHeightStyle, style]}
+      style={[
+        s.black,
+        typography,
+        isWeb && fontFamilyStyle,
+        lineHeightStyle,
+        style,
+      ]}
       // @ts-ignore web only -esb
       dataSet={Object.assign({tooltip: title}, dataSet || {})}
       selectable={selectable}
diff --git a/src/view/icons/index.tsx b/src/view/icons/index.tsx
index b7bbf1600..ede1e6335 100644
--- a/src/view/icons/index.tsx
+++ b/src/view/icons/index.tsx
@@ -103,6 +103,7 @@ import {faUsersSlash} from '@fortawesome/free-solid-svg-icons/faUsersSlash'
 import {faX} from '@fortawesome/free-solid-svg-icons/faX'
 import {faXmark} from '@fortawesome/free-solid-svg-icons/faXmark'
 import {faChevronDown} from '@fortawesome/free-solid-svg-icons/faChevronDown'
+import {faFilter} from '@fortawesome/free-solid-svg-icons/faFilter'
 
 library.add(
   faAddressCard,
@@ -208,4 +209,5 @@ library.add(
   faX,
   faXmark,
   faChevronDown,
+  faFilter,
 )
diff --git a/src/view/screens/AppPasswords.tsx b/src/view/screens/AppPasswords.tsx
index dc439c367..800216169 100644
--- a/src/view/screens/AppPasswords.tsx
+++ b/src/view/screens/AppPasswords.tsx
@@ -29,6 +29,8 @@ import {
 } from '#/state/queries/app-passwords'
 import {ErrorScreen} from '../com/util/error/ErrorScreen'
 import {cleanError} from '#/lib/strings/errors'
+import * as Prompt from '#/components/Prompt'
+import {useDialogControl} from '#/components/Dialog'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppPasswords'>
 export function AppPasswords({}: Props) {
@@ -212,23 +214,18 @@ function AppPassword({
 }) {
   const pal = usePalette('default')
   const {_} = useLingui()
-  const {openModal} = useModalControls()
+  const control = useDialogControl()
   const {contentLanguages} = useLanguagePrefs()
   const deleteMutation = useAppPasswordDeleteMutation()
 
   const onDelete = React.useCallback(async () => {
-    openModal({
-      name: 'confirm',
-      title: _(msg`Delete app password`),
-      message: _(
-        msg`Are you sure you want to delete the app password "${name}"?`,
-      ),
-      async onPressConfirm() {
-        await deleteMutation.mutateAsync({name})
-        Toast.show(_(msg`App password deleted`))
-      },
-    })
-  }, [deleteMutation, openModal, name, _])
+    await deleteMutation.mutateAsync({name})
+    Toast.show(_(msg`App password deleted`))
+  }, [deleteMutation, name, _])
+
+  const onPress = React.useCallback(() => {
+    control.open()
+  }, [control])
 
   const primaryLocale =
     contentLanguages.length > 0 ? contentLanguages[0] : 'en-US'
@@ -237,7 +234,7 @@ function AppPassword({
     <TouchableOpacity
       testID={testID}
       style={[styles.item, pal.border]}
-      onPress={onDelete}
+      onPress={onPress}
       accessibilityRole="button"
       accessibilityLabel={_(msg`Delete app password`)}
       accessibilityHint="">
@@ -260,6 +257,17 @@ function AppPassword({
         </Text>
       </View>
       <FontAwesomeIcon icon={['far', 'trash-can']} style={styles.trashIcon} />
+
+      <Prompt.Basic
+        control={control}
+        title={_(msg`Delete app password?`)}
+        description={_(
+          msg`Are you sure you want to delete the app password "${name}"?`,
+        )}
+        onConfirm={onDelete}
+        confirmButtonCta={_(msg`Delete`)}
+        confirmButtonColor="negative"
+      />
     </TouchableOpacity>
   )
 }
diff --git a/src/view/screens/DebugMod.tsx b/src/view/screens/DebugMod.tsx
new file mode 100644
index 000000000..64f2376a4
--- /dev/null
+++ b/src/view/screens/DebugMod.tsx
@@ -0,0 +1,923 @@
+import React from 'react'
+import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
+import {View} from 'react-native'
+import {
+  LABELS,
+  mock,
+  moderatePost,
+  moderateProfile,
+  ModerationOpts,
+  AppBskyActorDefs,
+  AppBskyFeedDefs,
+  AppBskyFeedPost,
+  LabelPreference,
+  ModerationDecision,
+  ModerationBehavior,
+  RichText,
+  ComAtprotoLabelDefs,
+  interpretLabelValueDefinition,
+} from '@atproto/api'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {moderationOptsOverrideContext} from '#/state/queries/preferences'
+import {useSession} from '#/state/session'
+import {FeedNotification} from '#/state/queries/notifications/types'
+import {
+  groupNotifications,
+  shouldFilterNotif,
+} from '#/state/queries/notifications/util'
+
+import {atoms as a, useTheme} from '#/alf'
+import {CenteredView, ScrollView} from '#/view/com/util/Views'
+import {H1, H3, P, Text} from '#/components/Typography'
+import {useGlobalLabelStrings} from '#/lib/moderation/useGlobalLabelStrings'
+import * as Toggle from '#/components/forms/Toggle'
+import * as ToggleButton from '#/components/forms/ToggleButton'
+import {Button, ButtonIcon, ButtonText} from '#/components/Button'
+import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
+import {
+  ChevronBottom_Stroke2_Corner0_Rounded as ChevronBottom,
+  ChevronTop_Stroke2_Corner0_Rounded as ChevronTop,
+} from '#/components/icons/Chevron'
+import {ScreenHider} from '../../components/moderation/ScreenHider'
+import {ProfileHeaderStandard} from '#/screens/Profile/Header/ProfileHeaderStandard'
+import {ProfileCard} from '../com/profile/ProfileCard'
+import {FeedItem} from '../com/posts/FeedItem'
+import {FeedItem as NotifFeedItem} from '../com/notifications/FeedItem'
+import {PostThreadItem} from '../com/post-thread/PostThreadItem'
+import {Divider} from '#/components/Divider'
+
+const LABEL_VALUES: (keyof typeof LABELS)[] = Object.keys(
+  LABELS,
+) as (keyof typeof LABELS)[]
+
+export const DebugModScreen = ({}: NativeStackScreenProps<
+  CommonNavigatorParams,
+  'DebugMod'
+>) => {
+  const t = useTheme()
+  const [scenario, setScenario] = React.useState<string[]>(['label'])
+  const [scenarioSwitches, setScenarioSwitches] = React.useState<string[]>([])
+  const [label, setLabel] = React.useState<string[]>([LABEL_VALUES[0]])
+  const [target, setTarget] = React.useState<string[]>(['account'])
+  const [visibility, setVisiblity] = React.useState<string[]>(['warn'])
+  const [customLabelDef, setCustomLabelDef] =
+    React.useState<ComAtprotoLabelDefs.LabelValueDefinition>({
+      identifier: 'custom',
+      blurs: 'content',
+      severity: 'alert',
+      defaultSetting: 'warn',
+      locales: [
+        {
+          lang: 'en',
+          name: 'Custom label',
+          description: 'A custom label created in this test environment',
+        },
+      ],
+    })
+  const [view, setView] = React.useState<string[]>(['post'])
+  const labelStrings = useGlobalLabelStrings()
+  const {currentAccount} = useSession()
+
+  const isTargetMe =
+    scenario[0] === 'label' && scenarioSwitches.includes('targetMe')
+  const isSelfLabel =
+    scenario[0] === 'label' && scenarioSwitches.includes('selfLabel')
+  const noAdult =
+    scenario[0] === 'label' && scenarioSwitches.includes('noAdult')
+  const isLoggedOut =
+    scenario[0] === 'label' && scenarioSwitches.includes('loggedOut')
+  const isFollowing = scenarioSwitches.includes('following')
+
+  const did =
+    isTargetMe && currentAccount ? currentAccount.did : 'did:web:bob.test'
+
+  const profile = React.useMemo(() => {
+    const mockedProfile = mock.profileViewBasic({
+      handle: `bob.test`,
+      displayName: 'Bob Robertson',
+      description: 'User with this as their bio',
+      labels:
+        scenario[0] === 'label' && target[0] === 'account'
+          ? [
+              mock.label({
+                src: isSelfLabel ? did : undefined,
+                val: label[0],
+                uri: `at://${did}/`,
+              }),
+            ]
+          : scenario[0] === 'label' && target[0] === 'profile'
+          ? [
+              mock.label({
+                src: isSelfLabel ? did : undefined,
+                val: label[0],
+                uri: `at://${did}/app.bsky.actor.profile/self`,
+              }),
+            ]
+          : undefined,
+      viewer: mock.actorViewerState({
+        following: isFollowing
+          ? `at://${currentAccount?.did || ''}/app.bsky.graph.follow/1234`
+          : undefined,
+        muted: scenario[0] === 'mute',
+        mutedByList: undefined,
+        blockedBy: undefined,
+        blocking:
+          scenario[0] === 'block'
+            ? `at://did:web:alice.test/app.bsky.actor.block/fake`
+            : undefined,
+        blockingByList: undefined,
+      }),
+    })
+    mockedProfile.did = did
+    mockedProfile.avatar = 'https://bsky.social/about/images/favicon-32x32.png'
+    mockedProfile.banner =
+      'https://bsky.social/about/images/social-card-default-gradient.png'
+    return mockedProfile
+  }, [scenario, target, label, isSelfLabel, did, isFollowing, currentAccount])
+
+  const post = React.useMemo(() => {
+    return mock.postView({
+      record: mock.post({
+        text: "This is the body of the post. It's where the text goes. You get the idea.",
+      }),
+      author: profile,
+      labels:
+        scenario[0] === 'label' && target[0] === 'post'
+          ? [
+              mock.label({
+                src: isSelfLabel ? did : undefined,
+                val: label[0],
+                uri: `at://${did}/app.bsky.feed.post/fake`,
+              }),
+            ]
+          : undefined,
+      embed:
+        target[0] === 'embed'
+          ? mock.embedRecordView({
+              record: mock.post({
+                text: 'Embed',
+              }),
+              labels:
+                scenario[0] === 'label' && target[0] === 'embed'
+                  ? [
+                      mock.label({
+                        src: isSelfLabel ? did : undefined,
+                        val: label[0],
+                        uri: `at://${did}/app.bsky.feed.post/fake`,
+                      }),
+                    ]
+                  : undefined,
+              author: profile,
+            })
+          : {
+              $type: 'app.bsky.embed.images#view',
+              images: [
+                {
+                  thumb:
+                    'https://bsky.social/about/images/social-card-default-gradient.png',
+                  fullsize:
+                    'https://bsky.social/about/images/social-card-default-gradient.png',
+                  alt: '',
+                },
+              ],
+            },
+    })
+  }, [scenario, label, target, profile, isSelfLabel, did])
+
+  const replyNotif = React.useMemo(() => {
+    const notif = mock.replyNotification({
+      record: mock.post({
+        text: "This is the body of the post. It's where the text goes. You get the idea.",
+        reply: {
+          parent: {
+            uri: `at://${did}/app.bsky.feed.post/fake-parent`,
+            cid: 'bafyreiclp443lavogvhj3d2ob2cxbfuscni2k5jk7bebjzg7khl3esabwq',
+          },
+          root: {
+            uri: `at://${did}/app.bsky.feed.post/fake-parent`,
+            cid: 'bafyreiclp443lavogvhj3d2ob2cxbfuscni2k5jk7bebjzg7khl3esabwq',
+          },
+        },
+      }),
+      author: profile,
+      labels:
+        scenario[0] === 'label' && target[0] === 'post'
+          ? [
+              mock.label({
+                src: isSelfLabel ? did : undefined,
+                val: label[0],
+                uri: `at://${did}/app.bsky.feed.post/fake`,
+              }),
+            ]
+          : undefined,
+    })
+    const [item] = groupNotifications([notif])
+    item.subject = mock.postView({
+      record: notif.record as AppBskyFeedPost.Record,
+      author: profile,
+      labels: notif.labels,
+    })
+    return item
+  }, [scenario, label, target, profile, isSelfLabel, did])
+
+  const followNotif = React.useMemo(() => {
+    const notif = mock.followNotification({
+      author: profile,
+      subjectDid: currentAccount?.did || '',
+    })
+    const [item] = groupNotifications([notif])
+    return item
+  }, [profile, currentAccount])
+
+  const modOpts = React.useMemo(() => {
+    return {
+      userDid: isLoggedOut ? '' : isTargetMe ? did : 'did:web:alice.test',
+      prefs: {
+        adultContentEnabled: !noAdult,
+        labels: {
+          [label[0]]: visibility[0] as LabelPreference,
+        },
+        labelers: [
+          {
+            did: 'did:plc:fake-labeler',
+            labels: {[label[0]]: visibility[0] as LabelPreference},
+          },
+        ],
+        mutedWords: [],
+        hiddenPosts: [],
+      },
+      labelDefs: {
+        'did:plc:fake-labeler': [
+          interpretLabelValueDefinition(customLabelDef, 'did:plc:fake-labeler'),
+        ],
+      },
+    }
+  }, [label, visibility, noAdult, isLoggedOut, isTargetMe, did, customLabelDef])
+
+  const profileModeration = React.useMemo(() => {
+    return moderateProfile(profile, modOpts)
+  }, [profile, modOpts])
+  const postModeration = React.useMemo(() => {
+    return moderatePost(post, modOpts)
+  }, [post, modOpts])
+
+  return (
+    <moderationOptsOverrideContext.Provider value={modOpts}>
+      <ScrollView>
+        <CenteredView style={[t.atoms.bg, a.px_lg, a.py_lg]}>
+          <H1 style={[a.text_5xl, a.font_bold, a.pb_lg]}>Moderation states</H1>
+
+          <Heading title="" subtitle="Scenario" />
+          <ToggleButton.Group
+            label="Scenario"
+            values={scenario}
+            onChange={setScenario}>
+            <ToggleButton.Button name="label" label="Label">
+              Label
+            </ToggleButton.Button>
+            <ToggleButton.Button name="block" label="Block">
+              Block
+            </ToggleButton.Button>
+            <ToggleButton.Button name="mute" label="Mute">
+              Mute
+            </ToggleButton.Button>
+          </ToggleButton.Group>
+
+          {scenario[0] === 'label' && (
+            <>
+              <View
+                style={[
+                  a.border,
+                  a.rounded_sm,
+                  a.mt_lg,
+                  a.mb_lg,
+                  a.p_lg,
+                  t.atoms.border_contrast_medium,
+                ]}>
+                <Toggle.Group
+                  label="Toggle"
+                  type="radio"
+                  values={label}
+                  onChange={setLabel}>
+                  <View style={[a.flex_row, a.gap_md, a.flex_wrap]}>
+                    {LABEL_VALUES.map(labelValue => {
+                      let targetFixed = target[0]
+                      if (
+                        targetFixed !== 'account' &&
+                        targetFixed !== 'profile'
+                      ) {
+                        targetFixed = 'content'
+                      }
+                      const disabled =
+                        isSelfLabel &&
+                        LABELS[labelValue].flags.includes('no-self')
+                      return (
+                        <Toggle.Item
+                          key={labelValue}
+                          name={labelValue}
+                          label={labelStrings[labelValue].name}
+                          disabled={disabled}
+                          style={disabled ? {opacity: 0.5} : undefined}>
+                          <Toggle.Radio />
+                          <Toggle.Label>{labelValue}</Toggle.Label>
+                        </Toggle.Item>
+                      )
+                    })}
+                    <Toggle.Item
+                      name="custom"
+                      label="Custom label"
+                      disabled={isSelfLabel}
+                      style={isSelfLabel ? {opacity: 0.5} : undefined}>
+                      <Toggle.Radio />
+                      <Toggle.Label>Custom label</Toggle.Label>
+                    </Toggle.Item>
+                  </View>
+                </Toggle.Group>
+
+                {label[0] === 'custom' ? (
+                  <CustomLabelForm
+                    def={customLabelDef}
+                    setDef={setCustomLabelDef}
+                  />
+                ) : (
+                  <>
+                    <View style={{height: 10}} />
+                    <Divider />
+                  </>
+                )}
+
+                <View style={{height: 10}} />
+
+                <SmallToggler label="Advanced">
+                  <Toggle.Group
+                    label="Toggle"
+                    type="checkbox"
+                    values={scenarioSwitches}
+                    onChange={setScenarioSwitches}>
+                    <View style={[a.gap_md, a.flex_row, a.flex_wrap, a.pt_md]}>
+                      <Toggle.Item name="targetMe" label="Target is me">
+                        <Toggle.Checkbox />
+                        <Toggle.Label>Target is me</Toggle.Label>
+                      </Toggle.Item>
+                      <Toggle.Item name="following" label="Following target">
+                        <Toggle.Checkbox />
+                        <Toggle.Label>Following target</Toggle.Label>
+                      </Toggle.Item>
+                      <Toggle.Item name="selfLabel" label="Self label">
+                        <Toggle.Checkbox />
+                        <Toggle.Label>Self label</Toggle.Label>
+                      </Toggle.Item>
+                      <Toggle.Item name="noAdult" label="Adult disabled">
+                        <Toggle.Checkbox />
+                        <Toggle.Label>Adult disabled</Toggle.Label>
+                      </Toggle.Item>
+                      <Toggle.Item name="loggedOut" label="Logged out">
+                        <Toggle.Checkbox />
+                        <Toggle.Label>Logged out</Toggle.Label>
+                      </Toggle.Item>
+                    </View>
+                  </Toggle.Group>
+
+                  {LABELS[label[0] as keyof typeof LABELS]?.configurable !==
+                    false && (
+                    <View style={[a.mt_md]}>
+                      <Text
+                        style={[a.font_bold, a.text_xs, t.atoms.text, a.pb_sm]}>
+                        Preference
+                      </Text>
+                      <Toggle.Group
+                        label="Preference"
+                        type="radio"
+                        values={visibility}
+                        onChange={setVisiblity}>
+                        <View
+                          style={[
+                            a.flex_row,
+                            a.gap_md,
+                            a.flex_wrap,
+                            a.align_center,
+                          ]}>
+                          <Toggle.Item name="hide" label="Hide">
+                            <Toggle.Radio />
+                            <Toggle.Label>Hide</Toggle.Label>
+                          </Toggle.Item>
+                          <Toggle.Item name="warn" label="Warn">
+                            <Toggle.Radio />
+                            <Toggle.Label>Warn</Toggle.Label>
+                          </Toggle.Item>
+                          <Toggle.Item name="ignore" label="Ignore">
+                            <Toggle.Radio />
+                            <Toggle.Label>Ignore</Toggle.Label>
+                          </Toggle.Item>
+                        </View>
+                      </Toggle.Group>
+                    </View>
+                  )}
+                </SmallToggler>
+              </View>
+
+              <View style={[a.flex_row, a.flex_wrap, a.gap_md]}>
+                <View>
+                  <Text
+                    style={[
+                      a.font_bold,
+                      a.text_xs,
+                      t.atoms.text,
+                      a.pl_md,
+                      a.pb_xs,
+                    ]}>
+                    Target
+                  </Text>
+                  <View
+                    style={[
+                      a.border,
+                      a.rounded_full,
+                      a.px_md,
+                      a.py_sm,
+                      t.atoms.border_contrast_medium,
+                      t.atoms.bg,
+                    ]}>
+                    <Toggle.Group
+                      label="Target"
+                      type="radio"
+                      values={target}
+                      onChange={setTarget}>
+                      <View style={[a.flex_row, a.gap_md, a.flex_wrap]}>
+                        <Toggle.Item name="account" label="Account">
+                          <Toggle.Radio />
+                          <Toggle.Label>Account</Toggle.Label>
+                        </Toggle.Item>
+                        <Toggle.Item name="profile" label="Profile">
+                          <Toggle.Radio />
+                          <Toggle.Label>Profile</Toggle.Label>
+                        </Toggle.Item>
+                        <Toggle.Item name="post" label="Post">
+                          <Toggle.Radio />
+                          <Toggle.Label>Post</Toggle.Label>
+                        </Toggle.Item>
+                        <Toggle.Item name="embed" label="Embed">
+                          <Toggle.Radio />
+                          <Toggle.Label>Embed</Toggle.Label>
+                        </Toggle.Item>
+                      </View>
+                    </Toggle.Group>
+                  </View>
+                </View>
+              </View>
+            </>
+          )}
+
+          <Spacer />
+
+          <Heading title="" subtitle="Results" />
+
+          <ToggleButton.Group label="Results" values={view} onChange={setView}>
+            <ToggleButton.Button name="post" label="Post">
+              Post
+            </ToggleButton.Button>
+            <ToggleButton.Button name="notifications" label="Notifications">
+              Notifications
+            </ToggleButton.Button>
+            <ToggleButton.Button name="account" label="Account">
+              Account
+            </ToggleButton.Button>
+            <ToggleButton.Button name="data" label="Data">
+              Data
+            </ToggleButton.Button>
+          </ToggleButton.Group>
+
+          <View
+            style={[
+              a.border,
+              a.rounded_sm,
+              a.mt_lg,
+              a.p_md,
+              t.atoms.border_contrast_medium,
+            ]}>
+            {view[0] === 'post' && (
+              <>
+                <Heading title="Post" subtitle="in feed" />
+                <MockPostFeedItem post={post} moderation={postModeration} />
+
+                <Heading title="Post" subtitle="viewed directly" />
+                <MockPostThreadItem post={post} moderation={postModeration} />
+
+                <Heading title="Post" subtitle="reply in thread" />
+                <MockPostThreadItem
+                  post={post}
+                  moderation={postModeration}
+                  reply
+                />
+              </>
+            )}
+
+            {view[0] === 'notifications' && (
+              <>
+                <Heading title="Notification" subtitle="quote or reply" />
+                <MockNotifItem notif={replyNotif} moderationOpts={modOpts} />
+                <View style={{height: 20}} />
+                <Heading title="Notification" subtitle="follow or like" />
+                <MockNotifItem notif={followNotif} moderationOpts={modOpts} />
+              </>
+            )}
+
+            {view[0] === 'account' && (
+              <>
+                <Heading title="Account" subtitle="in listing" />
+                <MockAccountCard
+                  profile={profile}
+                  moderation={profileModeration}
+                />
+
+                <Heading title="Account" subtitle="viewing directly" />
+                <MockAccountScreen
+                  profile={profile}
+                  moderation={profileModeration}
+                  moderationOpts={modOpts}
+                />
+              </>
+            )}
+
+            {view[0] === 'data' && (
+              <>
+                <ModerationUIView
+                  label="Profile Moderation UI"
+                  mod={profileModeration}
+                />
+                <ModerationUIView
+                  label="Post Moderation UI"
+                  mod={postModeration}
+                />
+                <DataView
+                  label={label[0]}
+                  data={LABELS[label[0] as keyof typeof LABELS]}
+                />
+                <DataView
+                  label="Profile Moderation Data"
+                  data={profileModeration}
+                />
+                <DataView label="Post Moderation Data" data={postModeration} />
+              </>
+            )}
+          </View>
+
+          <View style={{height: 400}} />
+        </CenteredView>
+      </ScrollView>
+    </moderationOptsOverrideContext.Provider>
+  )
+}
+
+function Heading({title, subtitle}: {title: string; subtitle?: string}) {
+  const t = useTheme()
+  return (
+    <H3 style={[a.text_3xl, a.font_bold, a.pb_md]}>
+      {title}{' '}
+      {!!subtitle && (
+        <H3 style={[t.atoms.text_contrast_medium, a.text_lg]}>{subtitle}</H3>
+      )}
+    </H3>
+  )
+}
+
+function CustomLabelForm({
+  def,
+  setDef,
+}: {
+  def: ComAtprotoLabelDefs.LabelValueDefinition
+  setDef: React.Dispatch<
+    React.SetStateAction<ComAtprotoLabelDefs.LabelValueDefinition>
+  >
+}) {
+  const t = useTheme()
+  return (
+    <View
+      style={[
+        a.flex_row,
+        a.flex_wrap,
+        a.gap_md,
+        t.atoms.bg_contrast_25,
+        a.rounded_md,
+        a.p_md,
+        a.mt_md,
+      ]}>
+      <View>
+        <Text style={[a.font_bold, a.text_xs, t.atoms.text, a.pl_md, a.pb_xs]}>
+          Blurs
+        </Text>
+        <View
+          style={[
+            a.border,
+            a.rounded_full,
+            a.px_md,
+            a.py_sm,
+            t.atoms.border_contrast_medium,
+            t.atoms.bg,
+          ]}>
+          <Toggle.Group
+            label="Blurs"
+            type="radio"
+            values={[def.blurs]}
+            onChange={values => setDef(v => ({...v, blurs: values[0]}))}>
+            <View style={[a.flex_row, a.gap_md, a.flex_wrap]}>
+              <Toggle.Item name="content" label="Content">
+                <Toggle.Radio />
+                <Toggle.Label>Content</Toggle.Label>
+              </Toggle.Item>
+              <Toggle.Item name="media" label="Media">
+                <Toggle.Radio />
+                <Toggle.Label>Media</Toggle.Label>
+              </Toggle.Item>
+              <Toggle.Item name="none" label="None">
+                <Toggle.Radio />
+                <Toggle.Label>None</Toggle.Label>
+              </Toggle.Item>
+            </View>
+          </Toggle.Group>
+        </View>
+      </View>
+      <View>
+        <Text style={[a.font_bold, a.text_xs, t.atoms.text, a.pl_md, a.pb_xs]}>
+          Severity
+        </Text>
+        <View
+          style={[
+            a.border,
+            a.rounded_full,
+            a.px_md,
+            a.py_sm,
+            t.atoms.border_contrast_medium,
+            t.atoms.bg,
+          ]}>
+          <Toggle.Group
+            label="Severity"
+            type="radio"
+            values={[def.severity]}
+            onChange={values => setDef(v => ({...v, severity: values[0]}))}>
+            <View style={[a.flex_row, a.gap_md, a.flex_wrap, a.align_center]}>
+              <Toggle.Item name="alert" label="Alert">
+                <Toggle.Radio />
+                <Toggle.Label>Alert</Toggle.Label>
+              </Toggle.Item>
+              <Toggle.Item name="inform" label="Inform">
+                <Toggle.Radio />
+                <Toggle.Label>Inform</Toggle.Label>
+              </Toggle.Item>
+              <Toggle.Item name="none" label="None">
+                <Toggle.Radio />
+                <Toggle.Label>None</Toggle.Label>
+              </Toggle.Item>
+            </View>
+          </Toggle.Group>
+        </View>
+      </View>
+    </View>
+  )
+}
+
+function Toggler({label, children}: React.PropsWithChildren<{label: string}>) {
+  const t = useTheme()
+  const [show, setShow] = React.useState(false)
+  return (
+    <View style={a.mb_md}>
+      <View
+        style={[
+          t.atoms.border_contrast_medium,
+          a.border,
+          a.rounded_sm,
+          a.p_xs,
+        ]}>
+        <Button
+          variant="solid"
+          color="secondary"
+          label="Toggle visibility"
+          size="small"
+          onPress={() => setShow(!show)}>
+          <ButtonText>{label}</ButtonText>
+          <ButtonIcon
+            icon={show ? ChevronTop : ChevronBottom}
+            position="right"
+          />
+        </Button>
+        {show && children}
+      </View>
+    </View>
+  )
+}
+
+function SmallToggler({
+  label,
+  children,
+}: React.PropsWithChildren<{label: string}>) {
+  const [show, setShow] = React.useState(false)
+  return (
+    <View>
+      <View style={[a.flex_row]}>
+        <Button
+          variant="ghost"
+          color="secondary"
+          label="Toggle visibility"
+          size="tiny"
+          onPress={() => setShow(!show)}>
+          <ButtonText>{label}</ButtonText>
+          <ButtonIcon
+            icon={show ? ChevronTop : ChevronBottom}
+            position="right"
+          />
+        </Button>
+      </View>
+      {show && children}
+    </View>
+  )
+}
+
+function DataView({label, data}: {label: string; data: any}) {
+  return (
+    <Toggler label={label}>
+      <Text style={[{fontFamily: 'monospace'}, a.p_md]}>
+        {JSON.stringify(data, null, 2)}
+      </Text>
+    </Toggler>
+  )
+}
+
+function ModerationUIView({
+  mod,
+  label,
+}: {
+  mod: ModerationDecision
+  label: string
+}) {
+  return (
+    <Toggler label={label}>
+      <View style={a.p_lg}>
+        {[
+          'profileList',
+          'profileView',
+          'avatar',
+          'banner',
+          'displayName',
+          'contentList',
+          'contentView',
+          'contentMedia',
+        ].map(key => {
+          const ui = mod.ui(key as keyof ModerationBehavior)
+          return (
+            <View key={key} style={[a.flex_row, a.gap_md]}>
+              <Text style={[a.font_bold, {width: 100}]}>{key}</Text>
+              <Flag v={ui.filter} label="Filter" />
+              <Flag v={ui.blur} label="Blur" />
+              <Flag v={ui.alert} label="Alert" />
+              <Flag v={ui.inform} label="Inform" />
+              <Flag v={ui.noOverride} label="No-override" />
+            </View>
+          )
+        })}
+      </View>
+    </Toggler>
+  )
+}
+
+function Spacer() {
+  return <View style={{height: 30}} />
+}
+
+function MockPostFeedItem({
+  post,
+  moderation,
+}: {
+  post: AppBskyFeedDefs.PostView
+  moderation: ModerationDecision
+}) {
+  const t = useTheme()
+  if (moderation.ui('contentList').filter) {
+    return (
+      <P style={[t.atoms.bg_contrast_25, a.px_lg, a.py_md, a.mb_lg]}>
+        Filtered from the feed
+      </P>
+    )
+  }
+  return (
+    <FeedItem
+      post={post}
+      record={post.record as AppBskyFeedPost.Record}
+      moderation={moderation}
+      reason={undefined}
+    />
+  )
+}
+
+function MockPostThreadItem({
+  post,
+  reply,
+}: {
+  post: AppBskyFeedDefs.PostView
+  moderation: ModerationDecision
+  reply?: boolean
+}) {
+  return (
+    <PostThreadItem
+      // @ts-ignore
+      post={post}
+      record={post.record as AppBskyFeedPost.Record}
+      depth={reply ? 1 : 0}
+      isHighlightedPost={!reply}
+      treeView={false}
+      prevPost={undefined}
+      nextPost={undefined}
+      hasPrecedingItem={false}
+      onPostReply={() => {}}
+    />
+  )
+}
+
+function MockNotifItem({
+  notif,
+  moderationOpts,
+}: {
+  notif: FeedNotification
+  moderationOpts: ModerationOpts
+}) {
+  const t = useTheme()
+  if (shouldFilterNotif(notif.notification, moderationOpts)) {
+    return (
+      <P style={[t.atoms.bg_contrast_25, a.px_lg, a.py_md]}>
+        Filtered from the feed
+      </P>
+    )
+  }
+  return <NotifFeedItem item={notif} moderationOpts={moderationOpts} />
+}
+
+function MockAccountCard({
+  profile,
+  moderation,
+}: {
+  profile: AppBskyActorDefs.ProfileViewBasic
+  moderation: ModerationDecision
+}) {
+  const t = useTheme()
+
+  if (moderation.ui('profileList').filter) {
+    return (
+      <P style={[t.atoms.bg_contrast_25, a.px_lg, a.py_md, a.mb_lg]}>
+        Filtered from the listing
+      </P>
+    )
+  }
+
+  return <ProfileCard profile={profile} />
+}
+
+function MockAccountScreen({
+  profile,
+  moderation,
+  moderationOpts,
+}: {
+  profile: AppBskyActorDefs.ProfileViewBasic
+  moderation: ModerationDecision
+  moderationOpts: ModerationOpts
+}) {
+  const t = useTheme()
+  const {_} = useLingui()
+  return (
+    <View style={[t.atoms.border_contrast_medium, a.border, a.mb_md]}>
+      <ScreenHider
+        style={{}}
+        screenDescription={_(msg`profile`)}
+        modui={moderation.ui('profileView')}>
+        <ProfileHeaderStandard
+          // @ts-ignore ProfileViewBasic is close enough -prf
+          profile={profile}
+          moderationOpts={moderationOpts}
+          descriptionRT={new RichText({text: profile.description as string})}
+        />
+      </ScreenHider>
+    </View>
+  )
+}
+
+function Flag({v, label}: {v: boolean | undefined; label: string}) {
+  const t = useTheme()
+  return (
+    <View style={[a.flex_row, a.align_center, a.gap_xs]}>
+      <View
+        style={[
+          a.justify_center,
+          a.align_center,
+          a.rounded_xs,
+          a.border,
+          t.atoms.border_contrast_medium,
+          {
+            backgroundColor: t.palette.contrast_25,
+            width: 14,
+            height: 14,
+          },
+        ]}>
+        {v && <Check size="xs" fill={t.palette.contrast_900} />}
+      </View>
+      <P style={a.text_xs}>{label}</P>
+    </View>
+  )
+}
diff --git a/src/view/screens/Feeds.tsx b/src/view/screens/Feeds.tsx
index 7216fd109..2e3bf08db 100644
--- a/src/view/screens/Feeds.tsx
+++ b/src/view/screens/Feeds.tsx
@@ -16,6 +16,7 @@ import {usePalette} from 'lib/hooks/usePalette'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {ComposeIcon2, CogIcon, MagnifyingGlassIcon2} from 'lib/icons'
 import {s} from 'lib/styles'
+import {atoms as a, useTheme} from '#/alf'
 import {SearchInput, SearchInputRef} from 'view/com/util/forms/SearchInput'
 import {UserAvatar} from 'view/com/util/UserAvatar'
 import {
@@ -41,8 +42,11 @@ import {
 import {cleanError} from 'lib/strings/errors'
 import {useComposerControls} from '#/state/shell/composer'
 import {useSession} from '#/state/session'
-import {isNative} from '#/platform/detection'
+import {isNative, isWeb} from '#/platform/detection'
 import {HITSLOP_10} from 'lib/constants'
+import {IconCircle} from '#/components/IconCircle'
+import {ListSparkle_Stroke2_Corner0_Rounded} from '#/components/icons/ListSparkle'
+import {ListMagnifyingGlass_Stroke2_Corner0_Rounded} from '#/components/icons/ListMagnifyingGlass'
 
 type Props = NativeStackScreenProps<FeedsTabNavigatorParams, 'Feeds'>
 
@@ -215,12 +219,7 @@ export function FeedsScreen(_props: Props) {
             // pendingItems: this.rootStore.preferences.savedFeeds.length || 3,
           })
         } else {
-          if (preferences?.feeds?.saved.length === 0) {
-            slices.push({
-              key: 'savedFeedNoResults',
-              type: 'savedFeedNoResults',
-            })
-          } else {
+          if (preferences?.feeds?.saved.length !== 0) {
             const {saved, pinned} = preferences.feeds
 
             slices = slices.concat(
@@ -400,46 +399,48 @@ export function FeedsScreen(_props: Props) {
       ) {
         return (
           <View style={s.p10}>
-            <ActivityIndicator />
+            <ActivityIndicator size="large" />
           </View>
         )
       } else if (item.type === 'savedFeedsHeader') {
-        if (!isMobile) {
-          return (
-            <View
-              style={[
-                pal.view,
-                styles.header,
-                pal.border,
-                {
-                  borderBottomWidth: 1,
-                },
-              ]}>
-              <Text type="title-lg" style={[pal.text, s.bold]}>
-                <Trans>My Feeds</Trans>
-              </Text>
-              <View style={styles.headerBtnGroup}>
-                <Pressable
-                  accessibilityRole="button"
-                  hitSlop={HITSLOP_10}
-                  onPress={searchInputRef.current?.focus}>
-                  <MagnifyingGlassIcon2
-                    size={22}
-                    strokeWidth={2}
-                    style={pal.icon}
-                  />
-                </Pressable>
-                <Link
-                  href="/settings/saved-feeds"
-                  accessibilityLabel={_(msg`Edit My Feeds`)}
-                  accessibilityHint="">
-                  <CogIcon strokeWidth={1.5} style={pal.icon} size={28} />
-                </Link>
+        return (
+          <>
+            {!isMobile && (
+              <View
+                style={[
+                  pal.view,
+                  styles.header,
+                  pal.border,
+                  {
+                    borderBottomWidth: 1,
+                  },
+                ]}>
+                <Text type="title-lg" style={[pal.text, s.bold]}>
+                  <Trans>Feeds</Trans>
+                </Text>
+                <View style={styles.headerBtnGroup}>
+                  <Pressable
+                    accessibilityRole="button"
+                    hitSlop={HITSLOP_10}
+                    onPress={searchInputRef.current?.focus}>
+                    <MagnifyingGlassIcon2
+                      size={22}
+                      strokeWidth={2}
+                      style={pal.icon}
+                    />
+                  </Pressable>
+                  <Link
+                    href="/settings/saved-feeds"
+                    accessibilityLabel={_(msg`Edit My Feeds`)}
+                    accessibilityHint="">
+                    <CogIcon strokeWidth={1.5} style={pal.icon} size={28} />
+                  </Link>
+                </View>
               </View>
-            </View>
-          )
-        }
-        return <View />
+            )}
+            {preferences?.feeds?.saved?.length !== 0 && <FeedsSavedHeader />}
+          </>
+        )
       } else if (item.type === 'savedFeedNoResults') {
         return (
           <View
@@ -457,47 +458,17 @@ export function FeedsScreen(_props: Props) {
       } else if (item.type === 'popularFeedsHeader') {
         return (
           <>
-            <View
-              style={[
-                pal.view,
-                styles.header,
-                {
-                  // This is first in the flatlist without a session -esb
-                  marginTop: hasSession ? 16 : 0,
-                  paddingLeft: isMobile ? 12 : undefined,
-                  paddingRight: 10,
-                  paddingBottom: isMobile ? 6 : undefined,
-                },
-              ]}>
-              <Text type="title-lg" style={[pal.text, s.bold]}>
-                <Trans>Discover new feeds</Trans>
-              </Text>
-
-              {!isMobile && (
-                <SearchInput
-                  ref={searchInputRef}
-                  query={query}
-                  onChangeQuery={onChangeQuery}
-                  onPressCancelSearch={onPressCancelSearch}
-                  onSubmitQuery={onSubmitQuery}
-                  setIsInputFocused={onChangeSearchFocus}
-                  style={{flex: 1, maxWidth: 250}}
-                />
-              )}
+            <FeedsAboutHeader />
+            <View style={{paddingHorizontal: 12, paddingBottom: 12}}>
+              <SearchInput
+                ref={searchInputRef}
+                query={query}
+                onChangeQuery={onChangeQuery}
+                onPressCancelSearch={onPressCancelSearch}
+                onSubmitQuery={onSubmitQuery}
+                setIsInputFocused={onChangeSearchFocus}
+              />
             </View>
-
-            {isMobile && (
-              <View style={{paddingHorizontal: 8, paddingBottom: 10}}>
-                <SearchInput
-                  ref={searchInputRef}
-                  query={query}
-                  onChangeQuery={onChangeQuery}
-                  onPressCancelSearch={onPressCancelSearch}
-                  onSubmitQuery={onSubmitQuery}
-                  setIsInputFocused={onChangeSearchFocus}
-                />
-              </View>
-            )}
           </>
         )
       } else if (item.type === 'popularFeedsLoading') {
@@ -529,15 +500,20 @@ export function FeedsScreen(_props: Props) {
       return null
     },
     [
-      _,
-      hasSession,
       isMobile,
-      pal,
+      pal.view,
+      pal.border,
+      pal.text,
+      pal.icon,
+      pal.textLight,
+      _,
+      preferences?.feeds?.saved?.length,
       query,
       onChangeQuery,
       onPressCancelSearch,
       onSubmitQuery,
       onChangeSearchFocus,
+      hasSession,
     ],
   )
 
@@ -552,8 +528,6 @@ export function FeedsScreen(_props: Props) {
         />
       )}
 
-      {preferences ? <View /> : <ActivityIndicator />}
-
       <List
         ref={listRef}
         style={[!isTabletOrDesktop && s.flex1, styles.list]}
@@ -660,6 +634,71 @@ function SavedFeedLoadingPlaceholder() {
   )
 }
 
+function FeedsSavedHeader() {
+  const t = useTheme()
+
+  return (
+    <View
+      style={
+        isWeb
+          ? [
+              a.flex_row,
+              a.px_md,
+              a.py_lg,
+              a.gap_md,
+              a.border_b,
+              t.atoms.border_contrast_low,
+            ]
+          : [
+              {flexDirection: 'row-reverse'},
+              a.p_lg,
+              a.gap_md,
+              a.border_b,
+              t.atoms.border_contrast_low,
+            ]
+      }>
+      <IconCircle icon={ListSparkle_Stroke2_Corner0_Rounded} size="lg" />
+      <View style={[a.flex_1, a.gap_xs]}>
+        <Text style={[a.flex_1, a.text_2xl, a.font_bold, t.atoms.text]}>
+          <Trans>My Feeds</Trans>
+        </Text>
+        <Text style={[t.atoms.text_contrast_high]}>
+          <Trans>All the feeds you've saved, right in one place.</Trans>
+        </Text>
+      </View>
+    </View>
+  )
+}
+
+function FeedsAboutHeader() {
+  const t = useTheme()
+
+  return (
+    <View
+      style={
+        isWeb
+          ? [a.flex_row, a.px_md, a.pt_lg, a.pb_lg, a.gap_md]
+          : [{flexDirection: 'row-reverse'}, a.p_lg, a.gap_md]
+      }>
+      <IconCircle
+        icon={ListMagnifyingGlass_Stroke2_Corner0_Rounded}
+        size="lg"
+      />
+      <View style={[a.flex_1, a.gap_sm]}>
+        <Text style={[a.flex_1, a.text_2xl, a.font_bold, t.atoms.text]}>
+          <Trans>Discover New Feeds</Trans>
+        </Text>
+        <Text style={[t.atoms.text_contrast_high]}>
+          <Trans>
+            Custom feeds built by the community bring you new experiences and
+            help you find the content you love.
+          </Trans>
+        </Text>
+      </View>
+    </View>
+  )
+}
+
 const styles = StyleSheet.create({
   container: {
     flex: 1,
diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx
index cb2abf1bc..99ac8c44a 100644
--- a/src/view/screens/Home.tsx
+++ b/src/view/screens/Home.tsx
@@ -6,7 +6,7 @@ import {FeedDescriptor, FeedParams} from '#/state/queries/post-feed'
 import {FollowingEmptyState} from 'view/com/posts/FollowingEmptyState'
 import {FollowingEndOfFeed} from 'view/com/posts/FollowingEndOfFeed'
 import {CustomFeedEmptyState} from 'view/com/posts/CustomFeedEmptyState'
-import {FeedsTabBar} from '../com/pager/FeedsTabBar'
+import {HomeHeader} from '../com/home/HomeHeader'
 import {Pager, RenderTabBarFnProps, PagerRef} from 'view/com/pager/Pager'
 import {FeedPage} from 'view/com/feeds/FeedPage'
 import {HomeLoggedOutCTA} from '../com/auth/HomeLoggedOutCTA'
@@ -17,11 +17,12 @@ import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
 import {emitSoftReset} from '#/state/events'
 import {useSession} from '#/state/session'
 import {useSelectedFeed, useSetSelectedFeed} from '#/state/shell/selected-feed'
+import {useSetTitle} from '#/lib/hooks/useSetTitle'
 
 type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'>
 export function HomeScreen(props: Props) {
   const {data: preferences} = usePreferencesQuery()
-  const {feeds: pinnedFeedInfos, isLoading: isPinnedFeedsLoading} =
+  const {data: pinnedFeedInfos, isLoading: isPinnedFeedsLoading} =
     usePinnedFeedsInfos()
   if (preferences && pinnedFeedInfos && !isPinnedFeedsLoading) {
     return (
@@ -66,6 +67,8 @@ function HomeScreenReady({
   const selectedIndex = Math.max(0, maybeFoundIndex)
   const selectedFeed = allFeeds[selectedIndex]
 
+  useSetTitle(pinnedFeedInfos[selectedIndex]?.displayName)
+
   const pagerRef = React.useRef<PagerRef>(null)
   const lastPagerReportedIndexRef = React.useRef(selectedIndex)
   React.useLayoutEffect(() => {
@@ -118,16 +121,16 @@ function HomeScreenReady({
   const renderTabBar = React.useCallback(
     (props: RenderTabBarFnProps) => {
       return (
-        <FeedsTabBar
+        <HomeHeader
           key="FEEDS_TAB_BAR"
-          selectedPage={props.selectedPage}
-          onSelect={props.onSelect}
+          {...props}
           testID="homeScreenFeedTabs"
           onPressSelected={onPressSelected}
+          feeds={pinnedFeedInfos}
         />
       )
     },
-    [onPressSelected],
+    [onPressSelected, pinnedFeedInfos],
   )
 
   const renderFollowingEmptyState = React.useCallback(() => {
diff --git a/src/view/screens/LanguageSettings.tsx b/src/view/screens/LanguageSettings.tsx
index 819840a46..b86cd46e1 100644
--- a/src/view/screens/LanguageSettings.tsx
+++ b/src/view/screens/LanguageSettings.tsx
@@ -97,7 +97,7 @@ export function LanguageSettingsScreen(_props: Props) {
           <Text style={[pal.text, s.pb10]}>
             <Trans>
               Select your app language for the default text to display in the
-              app
+              app.
             </Trans>
           </Text>
 
@@ -296,7 +296,7 @@ export function LanguageSettingsScreen(_props: Props) {
               type="button"
               style={[pal.text, {flexShrink: 1, overflow: 'hidden'}]}
               numberOfLines={1}>
-              {myLanguages.length ? myLanguages : 'Select languages'}
+              {myLanguages.length ? myLanguages : _(msg`Select languages`)}
             </Text>
           </Button>
         </View>
diff --git a/src/view/screens/Moderation.tsx b/src/view/screens/Moderation.tsx
deleted file mode 100644
index 8f1fe75b6..000000000
--- a/src/view/screens/Moderation.tsx
+++ /dev/null
@@ -1,285 +0,0 @@
-import React from 'react'
-import {
-  ActivityIndicator,
-  StyleSheet,
-  TouchableOpacity,
-  View,
-} from 'react-native'
-import {useFocusEffect} from '@react-navigation/native'
-import {
-  FontAwesomeIcon,
-  FontAwesomeIconStyle,
-} from '@fortawesome/react-native-fontawesome'
-import {ComAtprotoLabelDefs} from '@atproto/api'
-import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
-import {s} from 'lib/styles'
-import {CenteredView} from '../com/util/Views'
-import {ViewHeader} from '../com/util/ViewHeader'
-import {Link, TextLink} from '../com/util/Link'
-import {Text} from '../com/util/text/Text'
-import {usePalette} from 'lib/hooks/usePalette'
-import {useAnalytics} from 'lib/analytics/analytics'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {useSetMinimalShellMode} from '#/state/shell'
-import {useModalControls} from '#/state/modals'
-import {Trans, msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {ToggleButton} from '../com/util/forms/ToggleButton'
-import {useSession} from '#/state/session'
-import {
-  useProfileQuery,
-  useProfileUpdateMutation,
-} from '#/state/queries/profile'
-import {ScrollView} from '../com/util/Views'
-
-type Props = NativeStackScreenProps<CommonNavigatorParams, 'Moderation'>
-export function ModerationScreen({}: Props) {
-  const pal = usePalette('default')
-  const {_} = useLingui()
-  const setMinimalShellMode = useSetMinimalShellMode()
-  const {screen, track} = useAnalytics()
-  const {isTabletOrDesktop} = useWebMediaQueries()
-  const {openModal} = useModalControls()
-
-  useFocusEffect(
-    React.useCallback(() => {
-      screen('Moderation')
-      setMinimalShellMode(false)
-    }, [screen, setMinimalShellMode]),
-  )
-
-  const onPressContentFiltering = React.useCallback(() => {
-    track('Moderation:ContentfilteringButtonClicked')
-    openModal({name: 'content-filtering-settings'})
-  }, [track, openModal])
-
-  return (
-    <CenteredView
-      style={[
-        s.hContentRegion,
-        pal.border,
-        isTabletOrDesktop ? styles.desktopContainer : pal.viewLight,
-      ]}
-      testID="moderationScreen">
-      <ViewHeader title={_(msg`Moderation`)} showOnDesktop />
-      <ScrollView contentContainerStyle={[styles.noBorder]}>
-        <View style={styles.spacer} />
-        <TouchableOpacity
-          testID="contentFilteringBtn"
-          style={[styles.linkCard, pal.view]}
-          onPress={onPressContentFiltering}
-          accessibilityRole="tab"
-          accessibilityHint="Content filtering"
-          accessibilityLabel="">
-          <View style={[styles.iconContainer, pal.btn]}>
-            <FontAwesomeIcon
-              icon="eye"
-              style={pal.text as FontAwesomeIconStyle}
-            />
-          </View>
-          <Text type="lg" style={pal.text}>
-            <Trans>Content filtering</Trans>
-          </Text>
-        </TouchableOpacity>
-        <Link
-          testID="moderationlistsBtn"
-          style={[styles.linkCard, pal.view]}
-          href="/moderation/modlists">
-          <View style={[styles.iconContainer, pal.btn]}>
-            <FontAwesomeIcon
-              icon="users-slash"
-              style={pal.text as FontAwesomeIconStyle}
-            />
-          </View>
-          <Text type="lg" style={pal.text}>
-            <Trans>Moderation lists</Trans>
-          </Text>
-        </Link>
-        <Link
-          testID="mutedAccountsBtn"
-          style={[styles.linkCard, pal.view]}
-          href="/moderation/muted-accounts">
-          <View style={[styles.iconContainer, pal.btn]}>
-            <FontAwesomeIcon
-              icon="user-slash"
-              style={pal.text as FontAwesomeIconStyle}
-            />
-          </View>
-          <Text type="lg" style={pal.text}>
-            <Trans>Muted accounts</Trans>
-          </Text>
-        </Link>
-        <Link
-          testID="blockedAccountsBtn"
-          style={[styles.linkCard, pal.view]}
-          href="/moderation/blocked-accounts">
-          <View style={[styles.iconContainer, pal.btn]}>
-            <FontAwesomeIcon
-              icon="ban"
-              style={pal.text as FontAwesomeIconStyle}
-            />
-          </View>
-          <Text type="lg" style={pal.text}>
-            <Trans>Blocked accounts</Trans>
-          </Text>
-        </Link>
-        <Text
-          type="xl-bold"
-          style={[
-            pal.text,
-            {
-              paddingHorizontal: 18,
-              paddingTop: 18,
-              paddingBottom: 6,
-            },
-          ]}>
-          <Trans>Logged-out visibility</Trans>
-        </Text>
-        <PwiOptOut />
-      </ScrollView>
-    </CenteredView>
-  )
-}
-
-function PwiOptOut() {
-  const pal = usePalette('default')
-  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 (
-    <View style={[pal.view, styles.toggleCard]}>
-      <View
-        style={{flexDirection: 'row', alignItems: 'center', paddingRight: 14}}>
-        <ToggleButton
-          type="default-light"
-          label={_(
-            msg`Discourage apps from showing my account to logged-out users`,
-          )}
-          labelType="lg"
-          isSelected={isOptedOut}
-          onPress={canToggle ? onToggleOptOut : undefined}
-          style={[canToggle ? undefined : {opacity: 0.5}, {flex: 1}]}
-        />
-        {updateProfile.isPending && <ActivityIndicator />}
-      </View>
-      <View
-        style={{
-          flexDirection: 'column',
-          gap: 10,
-          paddingLeft: 66,
-          paddingRight: 12,
-          paddingBottom: 10,
-          marginBottom: 64,
-        }}>
-        <Text style={pal.textLight}>
-          <Trans>
-            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.
-          </Trans>
-        </Text>
-        <Text style={[pal.textLight, {fontWeight: '500'}]}>
-          <Trans>
-            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.
-          </Trans>
-        </Text>
-        <TextLink
-          style={pal.link}
-          href="https://blueskyweb.zendesk.com/hc/en-us/articles/15835264007693-Data-Privacy"
-          text={_(msg`Learn more about what is public on Bluesky.`)}
-        />
-      </View>
-    </View>
-  )
-}
-
-const styles = StyleSheet.create({
-  desktopContainer: {
-    borderLeftWidth: 1,
-    borderRightWidth: 1,
-  },
-  spacer: {
-    height: 6,
-  },
-  linkCard: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    paddingVertical: 12,
-    paddingHorizontal: 18,
-    marginBottom: 1,
-  },
-  toggleCard: {
-    paddingVertical: 8,
-    paddingTop: 2,
-    paddingHorizontal: 6,
-    marginBottom: 1,
-  },
-  iconContainer: {
-    alignItems: 'center',
-    justifyContent: 'center',
-    width: 40,
-    height: 40,
-    borderRadius: 30,
-    marginRight: 12,
-  },
-  noBorder: {
-    borderBottomWidth: 0,
-    borderRightWidth: 0,
-    borderLeftWidth: 0,
-    borderTopWidth: 0,
-  },
-})
diff --git a/src/view/screens/ModerationBlockedAccounts.tsx b/src/view/screens/ModerationBlockedAccounts.tsx
index 09d77987f..eb3b27048 100644
--- a/src/view/screens/ModerationBlockedAccounts.tsx
+++ b/src/view/screens/ModerationBlockedAccounts.tsx
@@ -131,7 +131,7 @@ export function ModerationBlockedAccounts({}: Props) {
               <Text type="lg" style={[pal.text, styles.emptyText]}>
                 <Trans>
                   You have not blocked any accounts yet. To block an account, go
-                  to their profile and selected "Block account" from the menu on
+                  to their profile and select "Block account" from the menu on
                   their account.
                 </Trans>
               </Text>
diff --git a/src/view/screens/ModerationMutedAccounts.tsx b/src/view/screens/ModerationMutedAccounts.tsx
index 1aff19dd3..911ace778 100644
--- a/src/view/screens/ModerationMutedAccounts.tsx
+++ b/src/view/screens/ModerationMutedAccounts.tsx
@@ -130,8 +130,8 @@ export function ModerationMutedAccounts({}: Props) {
               <Text type="lg" style={[pal.text, styles.emptyText]}>
                 <Trans>
                   You have not muted any accounts yet. To mute an account, go to
-                  their profile and selected "Mute account" from the menu on
-                  their account.
+                  their profile and select "Mute account" from the menu on their
+                  account.
                 </Trans>
               </Text>
             </View>
diff --git a/src/view/screens/NotFound.tsx b/src/view/screens/NotFound.tsx
index dfa840abb..7d51619b3 100644
--- a/src/view/screens/NotFound.tsx
+++ b/src/view/screens/NotFound.tsx
@@ -51,7 +51,13 @@ export const NotFoundScreen = () => {
         </Text>
         <Button
           type="primary"
-          label={canGoBack ? 'Go back' : 'Go home'}
+          label={canGoBack ? _(msg`Go Back`) : _(msg`Go Home`)}
+          accessibilityLabel={canGoBack ? _(msg`Go back`) : _(msg`Go home`)}
+          accessibilityHint={
+            canGoBack
+              ? _(msg`Returns to previous page`)
+              : _(msg`Returns to home page`)
+          }
           onPress={onPressHome}
         />
       </View>
diff --git a/src/view/screens/PostThread.tsx b/src/view/screens/PostThread.tsx
index aa09ab9ed..ba1fa130e 100644
--- a/src/view/screens/PostThread.tsx
+++ b/src/view/screens/PostThread.tsx
@@ -59,11 +59,7 @@ export function PostThreadScreen({route}: Props) {
         uri: thread.post.uri,
         cid: thread.post.cid,
         text: thread.record.text,
-        author: {
-          handle: thread.post.author.handle,
-          displayName: thread.post.author.displayName,
-          avatar: thread.post.author.avatar,
-        },
+        author: thread.post.author,
         embed: thread.post.embed,
       },
       onPost: () =>
diff --git a/src/view/screens/PreferencesHomeFeed.tsx b/src/view/screens/PreferencesFollowingFeed.tsx
index 7ad870937..b4acbcd44 100644
--- a/src/view/screens/PreferencesHomeFeed.tsx
+++ b/src/view/screens/PreferencesFollowingFeed.tsx
@@ -78,9 +78,9 @@ function RepliesThresholdInput({
 
 type Props = NativeStackScreenProps<
   CommonNavigatorParams,
-  'PreferencesHomeFeed'
+  'PreferencesFollowingFeed'
 >
-export function PreferencesHomeFeed({navigation}: Props) {
+export function PreferencesFollowingFeed({navigation}: Props) {
   const pal = usePalette('default')
   const {_} = useLingui()
   const {isTabletOrDesktop} = useWebMediaQueries()
@@ -101,14 +101,14 @@ export function PreferencesHomeFeed({navigation}: Props) {
         styles.container,
         isTabletOrDesktop && styles.desktopContainer,
       ]}>
-      <ViewHeader title={_(msg`Home Feed Preferences`)} showOnDesktop />
+      <ViewHeader title={_(msg`Following Feed Preferences`)} showOnDesktop />
       <View
         style={[
           styles.titleSection,
           isTabletOrDesktop && {paddingTop: 20, paddingBottom: 20},
         ]}>
         <Text type="xl" style={[pal.textLight, styles.description]}>
-          <Trans>Fine-tune the content you see on your home screen.</Trans>
+          <Trans>Fine-tune the content you see on your Following feed.</Trans>
         </Text>
       </View>
 
@@ -260,7 +260,7 @@ export function PreferencesHomeFeed({navigation}: Props) {
             <Text style={[pal.text, s.pb10]}>
               <Trans>
                 Set this setting to "Yes" to show samples of your saved feeds in
-                your following feed. This is an experimental feature.
+                your Following feed. This is an experimental feature.
               </Trans>
             </Text>
             <ToggleButton
diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx
index 64e067593..6073b9571 100644
--- a/src/view/screens/Profile.tsx
+++ b/src/view/screens/Profile.tsx
@@ -1,53 +1,45 @@
 import React, {useMemo} from 'react'
-import {StyleSheet, View} from 'react-native'
-import {useFocusEffect} from '@react-navigation/native'
+import {StyleSheet} from 'react-native'
 import {
   AppBskyActorDefs,
   moderateProfile,
   ModerationOpts,
   RichText as RichTextAPI,
 } from '@atproto/api'
-import {msg, Trans} from '@lingui/macro'
+import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
-import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
-import {CenteredView} from '../com/util/Views'
-import {ListRef} from '../com/util/List'
-import {ScreenHider} from 'view/com/util/moderation/ScreenHider'
-import {Feed} from 'view/com/posts/Feed'
-import {ProfileLists} from '../com/lists/ProfileLists'
-import {ProfileFeedgens} from '../com/feeds/ProfileFeedgens'
-import {ProfileHeader, ProfileHeaderLoading} from '../com/profile/ProfileHeader'
-import {PagerWithHeader} from 'view/com/pager/PagerWithHeader'
-import {ErrorScreen} from '../com/util/error/ErrorScreen'
-import {EmptyState} from '../com/util/EmptyState'
-import {FAB} from '../com/util/fab/FAB'
-import {s, colors} from 'lib/styles'
-import {useAnalytics} from 'lib/analytics/analytics'
-import {ComposeIcon2} from 'lib/icons'
-import {useSetTitle} from 'lib/hooks/useSetTitle'
-import {combinedDisplayName} from 'lib/strings/display-names'
-import {
-  FeedDescriptor,
-  resetProfilePostsQueries,
-} from '#/state/queries/post-feed'
-import {useResolveDidQuery} from '#/state/queries/resolve-uri'
-import {useProfileQuery} from '#/state/queries/profile'
+import {useFocusEffect} from '@react-navigation/native'
+import {useQueryClient} from '@tanstack/react-query'
+
+import {cleanError} from '#/lib/strings/errors'
+import {isInvalidHandle} from '#/lib/strings/handles'
 import {useProfileShadow} from '#/state/cache/profile-shadow'
-import {useSession, getAgent} from '#/state/session'
+import {listenSoftReset} from '#/state/events'
+import {useLabelerInfoQuery} from '#/state/queries/labeler'
+import {resetProfilePostsQueries} from '#/state/queries/post-feed'
 import {useModerationOpts} from '#/state/queries/preferences'
-import {useProfileExtraInfoQuery} from '#/state/queries/profile-extra-info'
-import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
+import {useProfileQuery} from '#/state/queries/profile'
+import {useResolveDidQuery} from '#/state/queries/resolve-uri'
+import {getAgent, useSession} from '#/state/session'
 import {useSetDrawerSwipeDisabled, useSetMinimalShellMode} from '#/state/shell'
-import {cleanError} from '#/lib/strings/errors'
-import {LoadLatestBtn} from '../com/util/load-latest/LoadLatestBtn'
-import {useQueryClient} from '@tanstack/react-query'
 import {useComposerControls} from '#/state/shell/composer'
-import {listenSoftReset} from '#/state/events'
-import {truncateAndInvalidate} from '#/state/queries/util'
-import {Text} from '#/view/com/util/text/Text'
-import {usePalette} from 'lib/hooks/usePalette'
-import {isNative} from '#/platform/detection'
-import {isInvalidHandle} from '#/lib/strings/handles'
+import {useAnalytics} from 'lib/analytics/analytics'
+import {useSetTitle} from 'lib/hooks/useSetTitle'
+import {ComposeIcon2} from 'lib/icons'
+import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
+import {combinedDisplayName} from 'lib/strings/display-names'
+import {colors, s} from 'lib/styles'
+import {PagerWithHeader} from 'view/com/pager/PagerWithHeader'
+import {ProfileHeader, ProfileHeaderLoading} from '#/screens/Profile/Header'
+import {ProfileFeedSection} from '#/screens/Profile/Sections/Feed'
+import {ProfileLabelsSection} from '#/screens/Profile/Sections/Labels'
+import {ScreenHider} from '#/components/moderation/ScreenHider'
+import {ProfileFeedgens} from '../com/feeds/ProfileFeedgens'
+import {ProfileLists} from '../com/lists/ProfileLists'
+import {ErrorScreen} from '../com/util/error/ErrorScreen'
+import {FAB} from '../com/util/fab/FAB'
+import {ListRef} from '../com/util/List'
+import {CenteredView} from '../com/util/Views'
 
 interface SectionRef {
   scrollToTop: () => void
@@ -57,6 +49,7 @@ type Props = NativeStackScreenProps<CommonNavigatorParams, 'Profile'>
 export function ProfileScreen({route}: Props) {
   const {_} = useLingui()
   const {currentAccount} = useSession()
+  const queryClient = useQueryClient()
   const name =
     route.params.name === 'me' ? currentAccount?.did : route.params.name
   const moderationOpts = useModerationOpts()
@@ -87,9 +80,9 @@ export function ProfileScreen({route}: Props) {
   // When we open the profile, we want to reset the posts query if we are blocked.
   React.useEffect(() => {
     if (resolvedDid && profile?.viewer?.blockedBy) {
-      resetProfilePostsQueries(resolvedDid)
+      resetProfilePostsQueries(queryClient, resolvedDid)
     }
-  }, [profile?.viewer?.blockedBy, resolvedDid])
+  }, [queryClient, profile?.viewer?.blockedBy, resolvedDid])
 
   // Most pushes will happen here, since we will have only placeholder data
   if (isLoadingDid || isLoadingProfile) {
@@ -148,16 +141,24 @@ function ProfileScreenLoaded({
   const setMinimalShellMode = useSetMinimalShellMode()
   const {openComposer} = useComposerControls()
   const {screen, track} = useAnalytics()
+  const {
+    data: labelerInfo,
+    error: labelerError,
+    isLoading: isLabelerLoading,
+  } = useLabelerInfoQuery({
+    did: profile.did,
+    enabled: !!profile.associated?.labeler,
+  })
   const [currentPage, setCurrentPage] = React.useState(0)
   const {_} = useLingui()
   const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
-  const extraInfoQuery = useProfileExtraInfoQuery(profile.did)
   const postsSectionRef = React.useRef<SectionRef>(null)
   const repliesSectionRef = React.useRef<SectionRef>(null)
   const mediaSectionRef = React.useRef<SectionRef>(null)
   const likesSectionRef = React.useRef<SectionRef>(null)
   const feedsSectionRef = React.useRef<SectionRef>(null)
   const listsSectionRef = React.useRef<SectionRef>(null)
+  const labelsSectionRef = React.useRef<SectionRef>(null)
 
   useSetTitle(combinedDisplayName(profile))
 
@@ -171,44 +172,75 @@ function ProfileScreenLoaded({
   )
 
   const isMe = profile.did === currentAccount?.did
+  const hasLabeler = !!profile.associated?.labeler
+  const showFiltersTab = hasLabeler
+  const showPostsTab = true
   const showRepliesTab = hasSession
+  const showMediaTab = !hasLabeler
   const showLikesTab = isMe
-  const showFeedsTab = hasSession && (isMe || extraInfoQuery.data?.hasFeedgens)
-  const showListsTab = hasSession && (isMe || extraInfoQuery.data?.hasLists)
+  const showFeedsTab =
+    hasSession && (isMe || (profile.associated?.feedgens || 0) > 0)
+  const showListsTab =
+    hasSession && (isMe || (profile.associated?.lists || 0) > 0)
+
   const sectionTitles = useMemo<string[]>(() => {
     return [
-      _(msg`Posts`),
+      showFiltersTab ? _(msg`Labels`) : undefined,
+      showListsTab && hasLabeler ? _(msg`Lists`) : undefined,
+      showPostsTab ? _(msg`Posts`) : undefined,
       showRepliesTab ? _(msg`Replies`) : undefined,
-      _(msg`Media`),
+      showMediaTab ? _(msg`Media`) : undefined,
       showLikesTab ? _(msg`Likes`) : undefined,
       showFeedsTab ? _(msg`Feeds`) : undefined,
-      showListsTab ? _(msg`Lists`) : undefined,
+      showListsTab && !hasLabeler ? _(msg`Lists`) : undefined,
     ].filter(Boolean) as string[]
-  }, [showRepliesTab, showLikesTab, showFeedsTab, showListsTab, _])
+  }, [
+    showPostsTab,
+    showRepliesTab,
+    showMediaTab,
+    showLikesTab,
+    showFeedsTab,
+    showListsTab,
+    showFiltersTab,
+    hasLabeler,
+    _,
+  ])
 
   let nextIndex = 0
-  const postsIndex = nextIndex++
+  let filtersIndex: number | null = null
+  let postsIndex: number | null = null
   let repliesIndex: number | null = null
+  let mediaIndex: number | null = null
+  let likesIndex: number | null = null
+  let feedsIndex: number | null = null
+  let listsIndex: number | null = null
+  if (showFiltersTab) {
+    filtersIndex = nextIndex++
+  }
+  if (showPostsTab) {
+    postsIndex = nextIndex++
+  }
   if (showRepliesTab) {
     repliesIndex = nextIndex++
   }
-  const mediaIndex = nextIndex++
-  let likesIndex: number | null = null
+  if (showMediaTab) {
+    mediaIndex = nextIndex++
+  }
   if (showLikesTab) {
     likesIndex = nextIndex++
   }
-  let feedsIndex: number | null = null
   if (showFeedsTab) {
     feedsIndex = nextIndex++
   }
-  let listsIndex: number | null = null
   if (showListsTab) {
     listsIndex = nextIndex++
   }
 
   const scrollSectionToTop = React.useCallback(
     (index: number) => {
-      if (index === postsIndex) {
+      if (index === filtersIndex) {
+        labelsSectionRef.current?.scrollToTop()
+      } else if (index === postsIndex) {
         postsSectionRef.current?.scrollToTop()
       } else if (index === repliesIndex) {
         repliesSectionRef.current?.scrollToTop()
@@ -222,7 +254,15 @@ function ProfileScreenLoaded({
         listsSectionRef.current?.scrollToTop()
       }
     },
-    [postsIndex, repliesIndex, mediaIndex, likesIndex, feedsIndex, listsIndex],
+    [
+      filtersIndex,
+      postsIndex,
+      repliesIndex,
+      mediaIndex,
+      likesIndex,
+      feedsIndex,
+      listsIndex,
+    ],
   )
 
   useFocusEffect(
@@ -278,6 +318,7 @@ function ProfileScreenLoaded({
     return (
       <ProfileHeader
         profile={profile}
+        labeler={labelerInfo}
         descriptionRT={hasDescription ? descriptionRT : null}
         moderationOpts={moderationOpts}
         hideBackButton={hideBackButton}
@@ -286,6 +327,7 @@ function ProfileScreenLoaded({
     )
   }, [
     profile,
+    labelerInfo,
     descriptionRT,
     hasDescription,
     moderationOpts,
@@ -297,8 +339,8 @@ function ProfileScreenLoaded({
     <ScreenHider
       testID="profileView"
       style={styles.container}
-      screenDescription="profile"
-      moderation={moderation.account}>
+      screenDescription={_(msg`profile`)}
+      modui={moderation.ui('profileView')}>
       <PagerWithHeader
         testID="profilePager"
         isHeaderReady={!showPlaceholder}
@@ -306,19 +348,45 @@ function ProfileScreenLoaded({
         onPageSelected={onPageSelected}
         onCurrentPageSelected={onCurrentPageSelected}
         renderHeader={renderHeader}>
-        {({headerHeight, isFocused, scrollElRef}) => (
-          <FeedSection
-            ref={postsSectionRef}
-            feed={`author|${profile.did}|posts_and_author_threads`}
-            headerHeight={headerHeight}
-            isFocused={isFocused}
-            scrollElRef={scrollElRef as ListRef}
-            ignoreFilterFor={profile.did}
-          />
-        )}
+        {showFiltersTab
+          ? ({headerHeight, scrollElRef}) => (
+              <ProfileLabelsSection
+                ref={labelsSectionRef}
+                labelerInfo={labelerInfo}
+                labelerError={labelerError}
+                isLabelerLoading={isLabelerLoading}
+                moderationOpts={moderationOpts}
+                scrollElRef={scrollElRef as ListRef}
+                headerHeight={headerHeight}
+              />
+            )
+          : null}
+        {showListsTab && !!profile.associated?.labeler
+          ? ({headerHeight, isFocused, scrollElRef}) => (
+              <ProfileLists
+                ref={listsSectionRef}
+                did={profile.did}
+                scrollElRef={scrollElRef as ListRef}
+                headerOffset={headerHeight}
+                enabled={isFocused}
+              />
+            )
+          : null}
+        {showPostsTab
+          ? ({headerHeight, isFocused, scrollElRef}) => (
+              <ProfileFeedSection
+                ref={postsSectionRef}
+                feed={`author|${profile.did}|posts_and_author_threads`}
+                headerHeight={headerHeight}
+                isFocused={isFocused}
+                scrollElRef={scrollElRef as ListRef}
+                ignoreFilterFor={profile.did}
+              />
+            )
+          : null}
         {showRepliesTab
           ? ({headerHeight, isFocused, scrollElRef}) => (
-              <FeedSection
+              <ProfileFeedSection
                 ref={repliesSectionRef}
                 feed={`author|${profile.did}|posts_with_replies`}
                 headerHeight={headerHeight}
@@ -328,19 +396,21 @@ function ProfileScreenLoaded({
               />
             )
           : null}
-        {({headerHeight, isFocused, scrollElRef}) => (
-          <FeedSection
-            ref={mediaSectionRef}
-            feed={`author|${profile.did}|posts_with_media`}
-            headerHeight={headerHeight}
-            isFocused={isFocused}
-            scrollElRef={scrollElRef as ListRef}
-            ignoreFilterFor={profile.did}
-          />
-        )}
+        {showMediaTab
+          ? ({headerHeight, isFocused, scrollElRef}) => (
+              <ProfileFeedSection
+                ref={mediaSectionRef}
+                feed={`author|${profile.did}|posts_with_media`}
+                headerHeight={headerHeight}
+                isFocused={isFocused}
+                scrollElRef={scrollElRef as ListRef}
+                ignoreFilterFor={profile.did}
+              />
+            )
+          : null}
         {showLikesTab
           ? ({headerHeight, isFocused, scrollElRef}) => (
-              <FeedSection
+              <ProfileFeedSection
                 ref={likesSectionRef}
                 feed={`likes|${profile.did}`}
                 headerHeight={headerHeight}
@@ -361,7 +431,7 @@ function ProfileScreenLoaded({
               />
             )
           : null}
-        {showListsTab
+        {showListsTab && !profile.associated?.labeler
           ? ({headerHeight, isFocused, scrollElRef}) => (
               <ProfileLists
                 ref={listsSectionRef}
@@ -387,77 +457,6 @@ function ProfileScreenLoaded({
   )
 }
 
-interface FeedSectionProps {
-  feed: FeedDescriptor
-  headerHeight: number
-  isFocused: boolean
-  scrollElRef: ListRef
-  ignoreFilterFor?: string
-}
-const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
-  function FeedSectionImpl(
-    {feed, headerHeight, isFocused, scrollElRef, ignoreFilterFor},
-    ref,
-  ) {
-    const {_} = useLingui()
-    const queryClient = useQueryClient()
-    const [hasNew, setHasNew] = React.useState(false)
-    const [isScrolledDown, setIsScrolledDown] = React.useState(false)
-
-    const onScrollToTop = React.useCallback(() => {
-      scrollElRef.current?.scrollToOffset({
-        animated: isNative,
-        offset: -headerHeight,
-      })
-      truncateAndInvalidate(queryClient, FEED_RQKEY(feed))
-      setHasNew(false)
-    }, [scrollElRef, headerHeight, queryClient, feed, setHasNew])
-    React.useImperativeHandle(ref, () => ({
-      scrollToTop: onScrollToTop,
-    }))
-
-    const renderPostsEmpty = React.useCallback(() => {
-      return <EmptyState icon="feed" message={_(msg`This feed is empty!`)} />
-    }, [_])
-
-    return (
-      <View>
-        <Feed
-          testID="postsFeed"
-          enabled={isFocused}
-          feed={feed}
-          scrollElRef={scrollElRef}
-          onHasNew={setHasNew}
-          onScrolledDownChange={setIsScrolledDown}
-          renderEmptyState={renderPostsEmpty}
-          headerOffset={headerHeight}
-          renderEndOfFeed={ProfileEndOfFeed}
-          ignoreFilterFor={ignoreFilterFor}
-        />
-        {(isScrolledDown || hasNew) && (
-          <LoadLatestBtn
-            onPress={onScrollToTop}
-            label={_(msg`Load new posts`)}
-            showIndicator={hasNew}
-          />
-        )}
-      </View>
-    )
-  },
-)
-
-function ProfileEndOfFeed() {
-  const pal = usePalette('default')
-
-  return (
-    <View style={[pal.border, {paddingTop: 32, borderTopWidth: 1}]}>
-      <Text style={[pal.textLight, pal.border, {textAlign: 'center'}]}>
-        <Trans>End of feed</Trans>
-      </Text>
-    </View>
-  )
-}
-
 function useRichText(text: string): [RichTextAPI, boolean] {
   const [prevText, setPrevText] = React.useState(text)
   const [rawRT, setRawRT] = React.useState(() => new RichTextAPI({text}))
@@ -491,6 +490,8 @@ const styles = StyleSheet.create({
   container: {
     flexDirection: 'column',
     height: '100%',
+    // @ts-ignore Web-only.
+    overflowAnchor: 'none', // Fixes jumps when switching tabs while scrolled down.
   },
   loading: {
     paddingVertical: 10,
diff --git a/src/view/screens/ProfileFeed.tsx b/src/view/screens/ProfileFeed.tsx
index be9eec816..8eeeb5d90 100644
--- a/src/view/screens/ProfileFeed.tsx
+++ b/src/view/screens/ProfileFeed.tsx
@@ -1,11 +1,9 @@
 import React, {useMemo, useCallback} from 'react'
-import {Dimensions, StyleSheet, View} from 'react-native'
+import {StyleSheet, View, Pressable} from 'react-native'
 import {NativeStackScreenProps} from '@react-navigation/native-stack'
 import {useIsFocused, useNavigation} from '@react-navigation/native'
 import {useQueryClient} from '@tanstack/react-query'
 import {usePalette} from 'lib/hooks/usePalette'
-import {HeartIcon, HeartIconSolid} from 'lib/icons'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {CommonNavigatorParams} from 'lib/routes/types'
 import {makeRecordUri} from 'lib/strings/url-helpers'
 import {s} from 'lib/styles'
@@ -13,11 +11,11 @@ import {FeedDescriptor} from '#/state/queries/post-feed'
 import {PagerWithHeader} from 'view/com/pager/PagerWithHeader'
 import {ProfileSubpageHeader} from 'view/com/profile/ProfileSubpageHeader'
 import {Feed} from 'view/com/posts/Feed'
-import {TextLink} from 'view/com/util/Link'
+import {InlineLink} from '#/components/Link'
 import {ListRef} from 'view/com/util/List'
 import {Button} from 'view/com/util/forms/Button'
 import {Text} from 'view/com/util/text/Text'
-import {RichText} from 'view/com/util/text/RichText'
+import {RichText} from '#/components/RichText'
 import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn'
 import {FAB} from 'view/com/util/fab/FAB'
 import {EmptyState} from 'view/com/util/EmptyState'
@@ -29,20 +27,15 @@ import {shareUrl} from 'lib/sharing'
 import {toShareUrl} from 'lib/strings/url-helpers'
 import {Haptics} from 'lib/haptics'
 import {useAnalytics} from 'lib/analytics/analytics'
-import {NativeDropdown, DropdownItem} from 'view/com/util/forms/NativeDropdown'
-import {useScrollHandlers} from '#/lib/ScrollContext'
-import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
 import {makeCustomFeedLink} from 'lib/routes/links'
 import {pluralize} from 'lib/strings/helpers'
-import {CenteredView, ScrollView} from 'view/com/util/Views'
+import {CenteredView} from 'view/com/util/Views'
 import {NavigationProp} from 'lib/routes/types'
-import {sanitizeHandle} from 'lib/strings/handles'
-import {makeProfileLink} from 'lib/routes/links'
 import {ComposeIcon2} from 'lib/icons'
 import {logger} from '#/logger'
 import {Trans, msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
-import {useModalControls} from '#/state/modals'
+import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog'
 import {useFeedSourceInfoQuery, FeedSourceFeedInfo} from '#/state/queries/feed'
 import {useResolveUriQuery} from '#/state/queries/resolve-uri'
 import {
@@ -59,8 +52,21 @@ import {useComposerControls} from '#/state/shell/composer'
 import {truncateAndInvalidate} from '#/state/queries/util'
 import {isNative} from '#/platform/detection'
 import {listenSoftReset} from '#/state/events'
+import {atoms as a, useTheme} from '#/alf'
+import * as Menu from '#/components/Menu'
+import {HITSLOP_20} from '#/lib/constants'
+import {DotGrid_Stroke2_Corner0_Rounded as Ellipsis} from '#/components/icons/DotGrid'
+import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
+import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
+import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
+import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox'
+import {
+  Heart2_Stroke2_Corner0_Rounded as HeartOutline,
+  Heart2_Filled_Stroke2_Corner0_Rounded as HeartFilled,
+} from '#/components/icons/Heart2'
+import {Button as NewButton, ButtonText} from '#/components/Button'
 
-const SECTION_TITLES = ['Posts', 'About']
+const SECTION_TITLES = ['Posts']
 
 interface SectionRef {
   scrollToTop: () => void
@@ -102,8 +108,8 @@ export function ProfileFeedScreen(props: Props) {
           <View style={{flexDirection: 'row'}}>
             <Button
               type="default"
-              accessibilityLabel={_(msg`Go Back`)}
-              accessibilityHint="Return to previous page"
+              accessibilityLabel={_(msg`Go back`)}
+              accessibilityHint={_(msg`Returns to previous page`)}
               onPress={onPressBack}
               style={{flexShrink: 1}}>
               <Text type="button" style={pal.text}>
@@ -147,9 +153,9 @@ export function ProfileFeedScreenInner({
   feedInfo: FeedSourceFeedInfo
 }) {
   const {_} = useLingui()
-  const pal = usePalette('default')
+  const t = useTheme()
   const {hasSession, currentAccount} = useSession()
-  const {openModal} = useModalControls()
+  const reportDialogControl = useReportDialogControl()
   const {openComposer} = useComposerControls()
   const {track} = useAnalytics()
   const feedSectionRef = React.useRef<SectionRef>(null)
@@ -199,9 +205,11 @@ export function ProfileFeedScreenInner({
       if (isSaved) {
         await removeFeed({uri: feedInfo.uri})
         resetRemoveFeed()
+        Toast.show(_(msg`Removed from your feeds`))
       } else {
         await saveFeed({uri: feedInfo.uri})
         resetSaveFeed()
+        Toast.show(_(msg`Saved to your feeds`))
       }
     } catch (err) {
       Toast.show(
@@ -245,13 +253,8 @@ export function ProfileFeedScreenInner({
   }, [feedInfo, track])
 
   const onPressReport = React.useCallback(() => {
-    if (!feedInfo) return
-    openModal({
-      name: 'report',
-      uri: feedInfo.uri,
-      cid: feedInfo.cid,
-    })
-  }, [openModal, feedInfo])
+    reportDialogControl.open()
+  }, [reportDialogControl])
 
   const onCurrentPageSelected = React.useCallback(
     (index: number) => {
@@ -262,134 +265,144 @@ export function ProfileFeedScreenInner({
     [feedSectionRef],
   )
 
-  // render
-  // =
-
-  const dropdownItems: DropdownItem[] = React.useMemo(() => {
-    return [
-      hasSession && {
-        testID: 'feedHeaderDropdownToggleSavedBtn',
-        label: isSaved ? _(msg`Remove from my feeds`) : _(msg`Add to my feeds`),
-        onPress: isSavePending || isRemovePending ? undefined : onToggleSaved,
-        icon: isSaved
-          ? {
-              ios: {
-                name: 'trash',
-              },
-              android: 'ic_delete',
-              web: ['far', 'trash-can'],
-            }
-          : {
-              ios: {
-                name: 'plus',
-              },
-              android: '',
-              web: 'plus',
-            },
-      },
-      hasSession && {
-        testID: 'feedHeaderDropdownReportBtn',
-        label: _(msg`Report feed`),
-        onPress: onPressReport,
-        icon: {
-          ios: {
-            name: 'exclamationmark.triangle',
-          },
-          android: 'ic_menu_report_image',
-          web: 'circle-exclamation',
-        },
-      },
-      {
-        testID: 'feedHeaderDropdownShareBtn',
-        label: _(msg`Share feed`),
-        onPress: onPressShare,
-        icon: {
-          ios: {
-            name: 'square.and.arrow.up',
-          },
-          android: 'ic_menu_share',
-          web: 'share',
-        },
-      },
-    ].filter(Boolean) as DropdownItem[]
-  }, [
-    hasSession,
-    onToggleSaved,
-    onPressReport,
-    onPressShare,
-    isSaved,
-    isSavePending,
-    isRemovePending,
-    _,
-  ])
-
   const renderHeader = useCallback(() => {
     return (
-      <ProfileSubpageHeader
-        isLoading={false}
-        href={feedInfo.route.href}
-        title={feedInfo?.displayName}
-        avatar={feedInfo?.avatar}
-        isOwner={feedInfo.creatorDid === currentAccount?.did}
-        creator={
-          feedInfo
-            ? {did: feedInfo.creatorDid, handle: feedInfo.creatorHandle}
-            : undefined
-        }
-        avatarType="algo">
-        {feedInfo && hasSession && (
-          <>
-            <Button
-              disabled={isSavePending || isRemovePending}
-              type="default"
-              label={isSaved ? _(msg`Unsave`) : _(msg`Save`)}
-              onPress={onToggleSaved}
-              style={styles.btn}
-            />
-            <Button
-              testID={isPinned ? 'unpinBtn' : 'pinBtn'}
-              disabled={isPinPending || isUnpinPending}
-              type={isPinned ? 'default' : 'inverted'}
-              label={isPinned ? _(msg`Unpin`) : _(msg`Pin to home`)}
-              onPress={onTogglePinned}
-              style={styles.btn}
-            />
-          </>
-        )}
-        <NativeDropdown
-          testID="headerDropdownBtn"
-          items={dropdownItems}
-          accessibilityLabel={_(msg`More options`)}
-          accessibilityHint="">
-          <View style={[pal.viewLight, styles.btn]}>
-            <FontAwesomeIcon
-              icon="ellipsis"
-              size={20}
-              color={pal.colors.text}
-            />
+      <>
+        <ProfileSubpageHeader
+          isLoading={false}
+          href={feedInfo.route.href}
+          title={feedInfo?.displayName}
+          avatar={feedInfo?.avatar}
+          isOwner={feedInfo.creatorDid === currentAccount?.did}
+          creator={
+            feedInfo
+              ? {did: feedInfo.creatorDid, handle: feedInfo.creatorHandle}
+              : undefined
+          }
+          avatarType="algo">
+          <View style={[a.flex_row, a.align_center, a.gap_sm]}>
+            {feedInfo && hasSession && (
+              <NewButton
+                testID={isPinned ? 'unpinBtn' : 'pinBtn'}
+                disabled={isPinPending || isUnpinPending}
+                size="small"
+                variant="solid"
+                color={isPinned ? 'secondary' : 'primary'}
+                label={isPinned ? _(msg`Unpin from home`) : _(msg`Pin to home`)}
+                onPress={onTogglePinned}>
+                <ButtonText>
+                  {isPinned ? _(msg`Unpin`) : _(msg`Pin to Home`)}
+                </ButtonText>
+              </NewButton>
+            )}
+            <Menu.Root>
+              <Menu.Trigger label={_(msg`Open feed options menu`)}>
+                {({props, state}) => {
+                  return (
+                    <Pressable
+                      {...props}
+                      hitSlop={HITSLOP_20}
+                      style={[
+                        a.justify_center,
+                        a.align_center,
+                        a.rounded_full,
+                        {height: 36, width: 36},
+                        t.atoms.bg_contrast_50,
+                        (state.hovered || state.pressed) && [
+                          t.atoms.bg_contrast_100,
+                        ],
+                      ]}
+                      testID="headerDropdownBtn">
+                      <Ellipsis
+                        size="lg"
+                        fill={t.atoms.text_contrast_medium.color}
+                      />
+                    </Pressable>
+                  )
+                }}
+              </Menu.Trigger>
+
+              <Menu.Outer>
+                <Menu.Group>
+                  {hasSession && (
+                    <>
+                      <Menu.Item
+                        disabled={isSavePending || isRemovePending}
+                        testID="feedHeaderDropdownToggleSavedBtn"
+                        label={
+                          isSaved
+                            ? _(msg`Remove from my feeds`)
+                            : _(msg`Save to my feeds`)
+                        }
+                        onPress={onToggleSaved}>
+                        <Menu.ItemText>
+                          {isSaved
+                            ? _(msg`Remove from my feeds`)
+                            : _(msg`Save to my feeds`)}
+                        </Menu.ItemText>
+                        <Menu.ItemIcon
+                          icon={isSaved ? Trash : Plus}
+                          position="right"
+                        />
+                      </Menu.Item>
+
+                      <Menu.Item
+                        testID="feedHeaderDropdownReportBtn"
+                        label={_(msg`Report feed`)}
+                        onPress={onPressReport}>
+                        <Menu.ItemText>{_(msg`Report feed`)}</Menu.ItemText>
+                        <Menu.ItemIcon icon={CircleInfo} position="right" />
+                      </Menu.Item>
+                    </>
+                  )}
+
+                  <Menu.Item
+                    testID="feedHeaderDropdownShareBtn"
+                    label={_(msg`Share feed`)}
+                    onPress={onPressShare}>
+                    <Menu.ItemText>{_(msg`Share feed`)}</Menu.ItemText>
+                    <Menu.ItemIcon icon={Share} position="right" />
+                  </Menu.Item>
+                </Menu.Group>
+              </Menu.Outer>
+            </Menu.Root>
           </View>
-        </NativeDropdown>
-      </ProfileSubpageHeader>
+        </ProfileSubpageHeader>
+        <AboutSection
+          feedOwnerDid={feedInfo.creatorDid}
+          feedRkey={feedInfo.route.params.rkey}
+          feedInfo={feedInfo}
+        />
+      </>
     )
   }, [
     _,
     hasSession,
-    pal,
     feedInfo,
     isPinned,
     onTogglePinned,
     onToggleSaved,
-    dropdownItems,
     currentAccount?.did,
     isPinPending,
     isRemovePending,
     isSavePending,
     isSaved,
     isUnpinPending,
+    onPressReport,
+    onPressShare,
+    t,
   ])
 
   return (
     <View style={s.hContentRegion}>
+      <ReportDialog
+        control={reportDialogControl}
+        params={{
+          type: 'feedgen',
+          uri: feedInfo.uri,
+          cid: feedInfo.cid,
+        }}
+      />
       <PagerWithHeader
         items={SECTION_TITLES}
         isHeaderReady={true}
@@ -404,18 +417,6 @@ export function ProfileFeedScreenInner({
             isFocused={isScreenFocused && isFocused}
           />
         )}
-        {({headerHeight, scrollElRef}) => (
-          <AboutSection
-            feedOwnerDid={feedInfo.creatorDid}
-            feedRkey={feedInfo.route.params.rkey}
-            feedInfo={feedInfo}
-            headerHeight={headerHeight}
-            scrollElRef={
-              scrollElRef as React.MutableRefObject<ScrollView | null>
-            }
-            isOwner={feedInfo.creatorDid === currentAccount?.did}
-          />
-        )}
       </PagerWithHeader>
       {hasSession && (
         <FAB
@@ -504,21 +505,14 @@ function AboutSection({
   feedOwnerDid,
   feedRkey,
   feedInfo,
-  headerHeight,
-  scrollElRef,
-  isOwner,
 }: {
   feedOwnerDid: string
   feedRkey: string
   feedInfo: FeedSourceFeedInfo
-  headerHeight: number
-  scrollElRef: React.MutableRefObject<ScrollView | null>
-  isOwner: boolean
 }) {
+  const t = useTheme()
   const pal = usePalette('default')
   const {_} = useLingui()
-  const scrollHandlers = useScrollHandlers()
-  const onScroll = useAnimatedScrollHandler(scrollHandlers)
   const [likeUri, setLikeUri] = React.useState(feedInfo.likeUri)
   const {hasSession} = useSession()
   const {track} = useAnalytics()
@@ -554,80 +548,47 @@ function AboutSection({
   }, [likeUri, isLiked, feedInfo, likeFeed, unlikeFeed, track, _])
 
   return (
-    <ScrollView
-      ref={scrollElRef}
-      onScroll={onScroll}
-      scrollEventThrottle={1}
-      contentContainerStyle={{
-        paddingTop: headerHeight,
-        minHeight: Dimensions.get('window').height * 1.5,
-      }}>
-      <View
-        style={[
-          {
-            borderTopWidth: 1,
-            paddingVertical: 20,
-            paddingHorizontal: 20,
-            gap: 12,
-          },
-          pal.border,
-        ]}>
+    <View style={[styles.aboutSectionContainer]}>
+      <View style={[a.pt_sm]}>
         {feedInfo.description ? (
           <RichText
             testID="listDescription"
-            type="lg"
-            style={pal.text}
-            richText={feedInfo.description}
+            style={[a.text_md]}
+            value={feedInfo.description}
           />
         ) : (
           <Text type="lg" style={[{fontStyle: 'italic'}, pal.textLight]}>
             <Trans>No description</Trans>
           </Text>
         )}
-        <View style={{flexDirection: 'row', alignItems: 'center', gap: 10}}>
-          <Button
-            type="default"
-            testID="toggleLikeBtn"
-            accessibilityLabel={_(msg`Like this feed`)}
-            accessibilityHint=""
-            disabled={!hasSession || isLikePending || isUnlikePending}
-            onPress={onToggleLiked}
-            style={{paddingHorizontal: 10}}>
-            {isLiked ? (
-              <HeartIconSolid size={19} style={s.likeColor} />
-            ) : (
-              <HeartIcon strokeWidth={3} size={19} style={pal.textLight} />
-            )}
-          </Button>
-          {typeof likeCount === 'number' && (
-            <TextLink
-              href={makeCustomFeedLink(feedOwnerDid, feedRkey, 'liked-by')}
-              text={_(
-                msg`Liked by ${likeCount} ${pluralize(likeCount, 'user')}`,
-              )}
-              style={[pal.textLight, s.semiBold]}
-            />
-          )}
-        </View>
-        <Text type="md" style={[pal.textLight]} numberOfLines={1}>
-          {isOwner ? (
-            <Trans>Created by you</Trans>
+      </View>
+
+      <View style={[a.flex_row, a.gap_sm, a.align_center, a.pb_sm]}>
+        <NewButton
+          size="small"
+          variant="solid"
+          color="secondary"
+          shape="round"
+          label={isLiked ? _(msg`Unlike this feed`) : _(msg`Like this feed`)}
+          testID="toggleLikeBtn"
+          disabled={!hasSession || isLikePending || isUnlikePending}
+          onPress={onToggleLiked}>
+          {isLiked ? (
+            <HeartFilled size="md" fill={s.likeColor.color} />
           ) : (
-            <Trans>
-              Created by{' '}
-              <TextLink
-                text={sanitizeHandle(feedInfo.creatorHandle, '@')}
-                href={makeProfileLink({
-                  did: feedInfo.creatorDid,
-                  handle: feedInfo.creatorHandle,
-                })}
-                style={pal.textLight}
-              />
-            </Trans>
+            <HeartOutline size="md" fill={t.atoms.text_contrast_medium.color} />
           )}
-        </Text>
+        </NewButton>
+        {typeof likeCount === 'number' && (
+          <InlineLink
+            label={_(msg`View users who like this feed`)}
+            to={makeCustomFeedLink(feedOwnerDid, feedRkey, 'liked-by')}
+            style={[t.atoms.text_contrast_medium, a.font_bold]}>
+            {_(msg`Liked by ${likeCount} ${pluralize(likeCount, 'user')}`)}
+          </InlineLink>
+        )}
       </View>
-    </ScrollView>
+    </View>
   )
 }
 
@@ -647,4 +608,9 @@ const styles = StyleSheet.create({
     paddingVertical: 14,
     borderRadius: 6,
   },
+  aboutSectionContainer: {
+    paddingVertical: 4,
+    paddingHorizontal: 16,
+    gap: 12,
+  },
 })
diff --git a/src/view/screens/ProfileFollowers.tsx b/src/view/screens/ProfileFollowers.tsx
index 2cad08cb5..6f8ecc2e8 100644
--- a/src/view/screens/ProfileFollowers.tsx
+++ b/src/view/screens/ProfileFollowers.tsx
@@ -21,7 +21,7 @@ export const ProfileFollowersScreen = ({route}: Props) => {
   )
 
   return (
-    <View>
+    <View style={{flex: 1}}>
       <ViewHeader title={_(msg`Followers`)} />
       <ProfileFollowersComponent name={name} />
     </View>
diff --git a/src/view/screens/ProfileFollows.tsx b/src/view/screens/ProfileFollows.tsx
index 80502b98b..bdab20153 100644
--- a/src/view/screens/ProfileFollows.tsx
+++ b/src/view/screens/ProfileFollows.tsx
@@ -21,7 +21,7 @@ export const ProfileFollowsScreen = ({route}: Props) => {
   )
 
   return (
-    <View>
+    <View style={{flex: 1}}>
       <ViewHeader title={_(msg`Following`)} />
       <ProfileFollowsComponent name={name} />
     </View>
diff --git a/src/view/screens/ProfileList.tsx b/src/view/screens/ProfileList.tsx
index 796464883..58b89f239 100644
--- a/src/view/screens/ProfileList.tsx
+++ b/src/view/screens/ProfileList.tsx
@@ -14,7 +14,7 @@ import {NativeDropdown, DropdownItem} from 'view/com/util/forms/NativeDropdown'
 import {CenteredView} from 'view/com/util/Views'
 import {EmptyState} from 'view/com/util/EmptyState'
 import {LoadingScreen} from 'view/com/util/LoadingScreen'
-import {RichText} from 'view/com/util/text/RichText'
+import {RichText} from '#/components/RichText'
 import {Button} from 'view/com/util/forms/Button'
 import {TextLink} from 'view/com/util/Link'
 import {ListRef} from 'view/com/util/List'
@@ -39,6 +39,7 @@ import {Trans, msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useSetMinimalShellMode} from '#/state/shell'
 import {useModalControls} from '#/state/modals'
+import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog'
 import {useResolveUriQuery} from '#/state/queries/resolve-uri'
 import {
   useListQuery,
@@ -60,6 +61,9 @@ import {
 import {logger} from '#/logger'
 import {useAnalytics} from '#/lib/analytics/analytics'
 import {listenSoftReset} from '#/state/events'
+import {atoms as a, useTheme} from '#/alf'
+import * as Prompt from '#/components/Prompt'
+import {useDialogControl} from '#/components/Dialog'
 
 const SECTION_TITLES_CURATE = ['Posts', 'About']
 const SECTION_TITLES_MOD = ['About']
@@ -233,7 +237,8 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
   const {_} = useLingui()
   const navigation = useNavigation<NavigationProp>()
   const {currentAccount} = useSession()
-  const {openModal, closeModal} = useModalControls()
+  const reportDialogControl = useReportDialogControl()
+  const {openModal} = useModalControls()
   const listMuteMutation = useListMuteMutation()
   const listBlockMutation = useListBlockMutation()
   const listDeleteMutation = useListDeleteMutation()
@@ -250,6 +255,10 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
   const {mutate: setSavedFeeds} = useSetSaveFeedsMutation()
   const {track} = useAnalytics()
 
+  const deleteListPromptControl = useDialogControl()
+  const subscribeMutePromptControl = useDialogControl()
+  const subscribeBlockPromptControl = useDialogControl()
+
   const isPinned = preferences?.feeds?.pinned?.includes(list.uri)
   const isSaved = preferences?.feeds?.saved?.includes(list.uri)
 
@@ -268,32 +277,19 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
     }
   }, [list.uri, isPinned, pinFeed, unpinFeed, _])
 
-  const onSubscribeMute = useCallback(() => {
-    openModal({
-      name: 'confirm',
-      title: _(msg`Mute these accounts?`),
-      message: _(
-        msg`Muting is private. Muted accounts can interact with you, but you will not see their posts or receive notifications from them.`,
-      ),
-      confirmBtnText: _(msg`Mute this List`),
-      async onPressConfirm() {
-        try {
-          await listMuteMutation.mutateAsync({uri: list.uri, mute: true})
-          Toast.show(_(msg`List muted`))
-          track('Lists:Mute')
-        } catch {
-          Toast.show(
-            _(
-              msg`There was an issue. Please check your internet connection and try again.`,
-            ),
-          )
-        }
-      },
-      onPressCancel() {
-        closeModal()
-      },
-    })
-  }, [openModal, closeModal, list, listMuteMutation, track, _])
+  const onSubscribeMute = useCallback(async () => {
+    try {
+      await listMuteMutation.mutateAsync({uri: list.uri, mute: true})
+      Toast.show(_(msg`List muted`))
+      track('Lists:Mute')
+    } catch {
+      Toast.show(
+        _(
+          msg`There was an issue. Please check your internet connection and try again.`,
+        ),
+      )
+    }
+  }, [list, listMuteMutation, track, _])
 
   const onUnsubscribeMute = useCallback(async () => {
     try {
@@ -309,32 +305,19 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
     }
   }, [list, listMuteMutation, track, _])
 
-  const onSubscribeBlock = useCallback(() => {
-    openModal({
-      name: 'confirm',
-      title: _(msg`Block these accounts?`),
-      message: _(
-        msg`Blocking is public. Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`,
-      ),
-      confirmBtnText: _(msg`Block this List`),
-      async onPressConfirm() {
-        try {
-          await listBlockMutation.mutateAsync({uri: list.uri, block: true})
-          Toast.show(_(msg`List blocked`))
-          track('Lists:Block')
-        } catch {
-          Toast.show(
-            _(
-              msg`There was an issue. Please check your internet connection and try again.`,
-            ),
-          )
-        }
-      },
-      onPressCancel() {
-        closeModal()
-      },
-    })
-  }, [openModal, closeModal, list, listBlockMutation, track, _])
+  const onSubscribeBlock = useCallback(async () => {
+    try {
+      await listBlockMutation.mutateAsync({uri: list.uri, block: true})
+      Toast.show(_(msg`List blocked`))
+      track('Lists:Block')
+    } catch {
+      Toast.show(
+        _(
+          msg`There was an issue. Please check your internet connection and try again.`,
+        ),
+      )
+    }
+  }, [list, listBlockMutation, track, _])
 
   const onUnsubscribeBlock = useCallback(async () => {
     try {
@@ -357,34 +340,26 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
     })
   }, [openModal, list])
 
-  const onPressDelete = useCallback(() => {
-    openModal({
-      name: 'confirm',
-      title: _(msg`Delete List`),
-      message: _(msg`Are you sure?`),
-      async onPressConfirm() {
-        await listDeleteMutation.mutateAsync({uri: list.uri})
-
-        if (isSaved || isPinned) {
-          const {saved, pinned} = preferences!.feeds
-
-          setSavedFeeds({
-            saved: isSaved ? saved.filter(uri => uri !== list.uri) : saved,
-            pinned: isPinned ? pinned.filter(uri => uri !== list.uri) : pinned,
-          })
-        }
+  const onPressDelete = useCallback(async () => {
+    await listDeleteMutation.mutateAsync({uri: list.uri})
 
-        Toast.show(_(msg`List deleted`))
-        track('Lists:Delete')
-        if (navigation.canGoBack()) {
-          navigation.goBack()
-        } else {
-          navigation.navigate('Home')
-        }
-      },
-    })
+    if (isSaved || isPinned) {
+      const {saved, pinned} = preferences!.feeds
+
+      setSavedFeeds({
+        saved: isSaved ? saved.filter(uri => uri !== list.uri) : saved,
+        pinned: isPinned ? pinned.filter(uri => uri !== list.uri) : pinned,
+      })
+    }
+
+    Toast.show(_(msg`List deleted`))
+    track('Lists:Delete')
+    if (navigation.canGoBack()) {
+      navigation.goBack()
+    } else {
+      navigation.navigate('Home')
+    }
   }, [
-    openModal,
     list,
     listDeleteMutation,
     navigation,
@@ -397,12 +372,8 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
   ])
 
   const onPressReport = useCallback(() => {
-    openModal({
-      name: 'report',
-      uri: list.uri,
-      cid: list.cid,
-    })
-  }, [openModal, list])
+    reportDialogControl.open()
+  }, [reportDialogControl])
 
   const onPressShare = useCallback(() => {
     const url = toShareUrl(`/profile/${list.creator.did}/lists/${rkey}`)
@@ -442,7 +413,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
       items.push({
         testID: 'listHeaderDropdownDeleteBtn',
         label: _(msg`Delete List`),
-        onPress: onPressDelete,
+        onPress: deleteListPromptControl.open,
         icon: {
           ios: {
             name: 'trash',
@@ -488,7 +459,9 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
         items.push({
           testID: 'listHeaderDropdownMuteBtn',
           label: isMuting ? _(msg`Un-mute list`) : _(msg`Mute list`),
-          onPress: isMuting ? onUnsubscribeMute : onSubscribeMute,
+          onPress: isMuting
+            ? onUnsubscribeMute
+            : subscribeMutePromptControl.open,
           icon: {
             ios: {
               name: isMuting ? 'eye' : 'eye.slash',
@@ -503,7 +476,9 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
         items.push({
           testID: 'listHeaderDropdownBlockBtn',
           label: isBlocking ? _(msg`Un-block list`) : _(msg`Block list`),
-          onPress: isBlocking ? onUnsubscribeBlock : onSubscribeBlock,
+          onPress: isBlocking
+            ? onUnsubscribeBlock
+            : subscribeBlockPromptControl.open,
           icon: {
             ios: {
               name: 'person.fill.xmark',
@@ -516,24 +491,24 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
     }
     return items
   }, [
-    isOwner,
-    onPressShare,
-    onPressEdit,
-    onPressDelete,
-    onPressReport,
     _,
+    onPressShare,
+    isOwner,
     isModList,
     isPinned,
-    unpinFeed,
+    isCurateList,
+    onPressEdit,
+    deleteListPromptControl.open,
+    onPressReport,
     isPending,
+    unpinFeed,
     list.uri,
-    isCurateList,
-    isMuting,
     isBlocking,
+    isMuting,
     onUnsubscribeMute,
-    onSubscribeMute,
+    subscribeMutePromptControl.open,
     onUnsubscribeBlock,
-    onSubscribeBlock,
+    subscribeBlockPromptControl.open,
   ])
 
   const subscribeDropdownItems: DropdownItem[] = useMemo(() => {
@@ -541,7 +516,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
       {
         testID: 'subscribeDropdownMuteBtn',
         label: _(msg`Mute accounts`),
-        onPress: onSubscribeMute,
+        onPress: subscribeMutePromptControl.open,
         icon: {
           ios: {
             name: 'speaker.slash',
@@ -553,7 +528,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
       {
         testID: 'subscribeDropdownBlockBtn',
         label: _(msg`Block accounts`),
-        onPress: onSubscribeBlock,
+        onPress: subscribeBlockPromptControl.open,
         icon: {
           ios: {
             name: 'person.fill.xmark',
@@ -563,7 +538,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
         },
       },
     ]
-  }, [onSubscribeMute, onSubscribeBlock, _])
+  }, [_, subscribeMutePromptControl.open, subscribeBlockPromptControl.open])
 
   return (
     <ProfileSubpageHeader
@@ -573,6 +548,14 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
       isOwner={list.creator.did === currentAccount?.did}
       creator={list.creator}
       avatarType="list">
+      <ReportDialog
+        control={reportDialogControl}
+        params={{
+          type: 'list',
+          uri: list.uri,
+          cid: list.cid,
+        }}
+      />
       {isCurateList || isPinned ? (
         <Button
           testID={isPinned ? 'unpinBtn' : 'pinBtn'}
@@ -619,6 +602,38 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
           <FontAwesomeIcon icon="ellipsis" size={20} color={pal.colors.text} />
         </View>
       </NativeDropdown>
+
+      <Prompt.Basic
+        control={deleteListPromptControl}
+        title={_(msg`Delete this list?`)}
+        description={_(
+          msg`If you delete this list, you won't be able to recover it.`,
+        )}
+        onConfirm={onPressDelete}
+        confirmButtonCta={_(msg`Delete`)}
+        confirmButtonColor="negative"
+      />
+
+      <Prompt.Basic
+        control={subscribeMutePromptControl}
+        title={_(msg`Mute these accounts?`)}
+        description={_(
+          msg`Muting is private. Muted accounts can interact with you, but you will not see their posts or receive notifications from them.`,
+        )}
+        onConfirm={onSubscribeMute}
+        confirmButtonCta={_(msg`Mute list`)}
+      />
+
+      <Prompt.Basic
+        control={subscribeBlockPromptControl}
+        title={_(msg`Block these accounts?`)}
+        description={_(
+          msg`Blocking is public. Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`,
+        )}
+        onConfirm={onSubscribeBlock}
+        confirmButtonCta={_(msg`Block list`)}
+        confirmButtonColor="negative"
+      />
     </ProfileSubpageHeader>
   )
 }
@@ -698,6 +713,7 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
     ref,
   ) {
     const pal = usePalette('default')
+    const t = useTheme()
     const {_} = useLingui()
     const {isMobile} = useWebMediaQueries()
     const {currentAccount} = useSession()
@@ -742,9 +758,8 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
             {descriptionRT ? (
               <RichText
                 testID="listDescription"
-                type="lg"
-                style={pal.text}
-                richText={descriptionRT}
+                style={[a.text_md]}
+                value={descriptionRT}
               />
             ) : (
               <Text
@@ -792,7 +807,7 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
                 paddingBottom: isMobile ? 14 : 18,
               },
             ]}>
-            <Text type="lg-bold">
+            <Text type="lg-bold" style={t.atoms.text}>
               <Trans>Users</Trans>
             </Text>
             {isOwner && (
@@ -817,14 +832,18 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
         </View>
       )
     }, [
-      pal,
-      list,
       isMobile,
+      pal.border,
+      pal.textLight,
+      pal.colors.link,
+      pal.link,
       descriptionRT,
       isCurateList,
       isOwner,
-      onPressAddUser,
+      list.creator,
+      t.atoms.text,
       _,
+      onPressAddUser,
     ])
 
     const renderEmptyState = useCallback(() => {
@@ -894,7 +913,7 @@ function ErrorScreen({error}: {error: string}) {
       <View style={{flexDirection: 'row'}}>
         <Button
           type="default"
-          accessibilityLabel={_(msg`Go Back`)}
+          accessibilityLabel={_(msg`Go back`)}
           accessibilityHint={_(msg`Return to previous page`)}
           onPress={onPressBack}
           style={{flexShrink: 1}}>
diff --git a/src/view/screens/Search/Search.tsx b/src/view/screens/Search/Search.tsx
index 142726701..c0f4cf195 100644
--- a/src/view/screens/Search/Search.tsx
+++ b/src/view/screens/Search/Search.tsx
@@ -1,58 +1,60 @@
 import React from 'react'
 import {
-  View,
-  StyleSheet,
   ActivityIndicator,
-  TextInput,
-  Pressable,
   Platform,
+  Pressable,
+  StyleSheet,
+  TextInput,
+  View,
 } from 'react-native'
-import {ScrollView, CenteredView} from '#/view/com/util/Views'
-import {List} from '#/view/com/util/List'
 import {AppBskyActorDefs, AppBskyFeedDefs, moderateProfile} from '@atproto/api'
-import {msg, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
 import {
   FontAwesomeIcon,
   FontAwesomeIconStyle,
 } from '@fortawesome/react-native-fontawesome'
-import {useFocusEffect} from '@react-navigation/native'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import AsyncStorage from '@react-native-async-storage/async-storage'
+import {useFocusEffect, useNavigation} from '@react-navigation/native'
 
+import {useAnalytics} from '#/lib/analytics/analytics'
+import {HITSLOP_10} from '#/lib/constants'
+import {usePalette} from '#/lib/hooks/usePalette'
+import {MagnifyingGlassIcon} from '#/lib/icons'
+import {NavigationProp} from '#/lib/routes/types'
+import {augmentSearchQuery} from '#/lib/strings/helpers'
+import {s} from '#/lib/styles'
 import {logger} from '#/logger'
+import {isNative, isWeb} from '#/platform/detection'
+import {listenSoftReset} from '#/state/events'
+import {useActorAutocompleteFn} from '#/state/queries/actor-autocomplete'
+import {useActorSearch} from '#/state/queries/actor-search'
+import {useModerationOpts} from '#/state/queries/preferences'
+import {useSearchPostsQuery} from '#/state/queries/search-posts'
+import {useGetSuggestedFollowersByActor} from '#/state/queries/suggested-follows'
+import {useSession} from '#/state/session'
+import {useSetDrawerOpen} from '#/state/shell'
+import {useSetDrawerSwipeDisabled, useSetMinimalShellMode} from '#/state/shell'
+import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {
   NativeStackScreenProps,
   SearchTabNavigatorParams,
 } from 'lib/routes/types'
-import {Text} from '#/view/com/util/text/Text'
-import {ProfileCardFeedLoadingPlaceholder} from 'view/com/util/LoadingPlaceholder'
-import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
-import {Post} from '#/view/com/post/Post'
+import {useTheme} from 'lib/ThemeContext'
 import {Pager} from '#/view/com/pager/Pager'
 import {TabBar} from '#/view/com/pager/TabBar'
-import {HITSLOP_10} from '#/lib/constants'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {usePalette} from '#/lib/hooks/usePalette'
-import {useTheme} from 'lib/ThemeContext'
-import {useSession} from '#/state/session'
-import {useGetSuggestedFollowersByActor} from '#/state/queries/suggested-follows'
-import {useSearchPostsQuery} from '#/state/queries/search-posts'
-import {useActorSearch} from '#/state/queries/actor-search'
-import {useActorAutocompleteFn} from '#/state/queries/actor-autocomplete'
-import {useSetDrawerOpen} from '#/state/shell'
-import {useAnalytics} from '#/lib/analytics/analytics'
-import {MagnifyingGlassIcon} from '#/lib/icons'
-import {useModerationOpts} from '#/state/queries/preferences'
+import {Post} from '#/view/com/post/Post'
+import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
+import {List} from '#/view/com/util/List'
+import {Text} from '#/view/com/util/text/Text'
+import {CenteredView, ScrollView} from '#/view/com/util/Views'
 import {
   MATCH_HANDLE,
   SearchLinkCard,
   SearchProfileCard,
 } from '#/view/shell/desktop/Search'
-import {useSetMinimalShellMode, useSetDrawerSwipeDisabled} from '#/state/shell'
-import {isNative, isWeb} from '#/platform/detection'
-import {listenSoftReset} from '#/state/events'
-import {s} from '#/lib/styles'
-import AsyncStorage from '@react-native-async-storage/async-storage'
-import {augmentSearchQuery} from '#/lib/strings/helpers'
+import {ProfileCardFeedLoadingPlaceholder} from 'view/com/util/LoadingPlaceholder'
+import {atoms as a} from '#/alf'
 
 function Loader() {
   const pal = usePalette('default')
@@ -140,6 +142,7 @@ function SearchScreenSuggestedFollows() {
         friends.slice(0, 4).map(friend =>
           getSuggestedFollowsByActor(friend.did).then(foafsRes => {
             for (const user of foafsRes.suggestions) {
+              if (user.associated?.labeler) continue
               friendsOfFriends.set(user.did, user)
             }
           }),
@@ -448,6 +451,7 @@ export function SearchScreenInner({
 export function SearchScreen(
   props: NativeStackScreenProps<SearchTabNavigatorParams, 'Search'>,
 ) {
+  const navigation = useNavigation<NavigationProp>()
   const theme = useTheme()
   const textInput = React.useRef<TextInput>(null)
   const {_} = useLingui()
@@ -472,6 +476,27 @@ export function SearchScreen(
     React.useState(false)
   const [searchHistory, setSearchHistory] = React.useState<string[]>([])
 
+  /**
+   * The Search screen's `q` param
+   */
+  const queryParam = props.route?.params?.q
+
+  /**
+   * If `true`, this means we received new instructions from the router. This
+   * is handled in a effect, and used to update the value of `query` locally
+   * within this screen.
+   */
+  const routeParamsMismatch = queryParam && queryParam !== query
+
+  React.useEffect(() => {
+    if (queryParam && routeParamsMismatch) {
+      // reset immediately and let local state take over
+      navigation.setParams({q: ''})
+      // update query for next search
+      setQuery(queryParam)
+    }
+  }, [queryParam, routeParamsMismatch, navigation])
+
   React.useEffect(() => {
     const loadSearchHistory = async () => {
       try {
@@ -749,19 +774,27 @@ export function SearchScreen(
             {searchHistory.length > 0 && (
               <View style={styles.searchHistoryContent}>
                 <Text style={[pal.text, styles.searchHistoryTitle]}>
-                  Recent Searches
+                  <Trans>Recent Searches</Trans>
                 </Text>
                 {searchHistory.map((historyItem, index) => (
-                  <View key={index} style={styles.historyItemContainer}>
+                  <View
+                    key={index}
+                    style={[
+                      a.flex_row,
+                      a.mt_md,
+                      a.justify_center,
+                      a.justify_between,
+                    ]}>
                     <Pressable
                       accessibilityRole="button"
                       onPress={() => handleHistoryItemClick(historyItem)}
-                      style={styles.historyItem}>
+                      style={[a.flex_1, a.py_sm]}>
                       <Text style={pal.text}>{historyItem}</Text>
                     </Pressable>
                     <Pressable
                       accessibilityRole="button"
-                      onPress={() => handleRemoveHistoryItem(historyItem)}>
+                      onPress={() => handleRemoveHistoryItem(historyItem)}
+                      style={[a.px_md, a.py_xs, a.justify_center]}>
                       <FontAwesomeIcon
                         icon="xmark"
                         size={16}
@@ -774,6 +807,8 @@ export function SearchScreen(
             )}
           </View>
         </CenteredView>
+      ) : routeParamsMismatch ? (
+        <ActivityIndicator />
       ) : (
         <SearchScreenInner query={query} />
       )}
@@ -846,13 +881,4 @@ const styles = StyleSheet.create({
   searchHistoryTitle: {
     fontWeight: 'bold',
   },
-  historyItem: {
-    paddingVertical: 8,
-  },
-  historyItemContainer: {
-    flexDirection: 'row',
-    justifyContent: 'space-between',
-    alignItems: 'center',
-    paddingVertical: 8,
-  },
 })
diff --git a/src/view/screens/Settings/ExportCarDialog.tsx b/src/view/screens/Settings/ExportCarDialog.tsx
index 720cd4f09..ba8fad2df 100644
--- a/src/view/screens/Settings/ExportCarDialog.tsx
+++ b/src/view/screens/Settings/ExportCarDialog.tsx
@@ -76,10 +76,11 @@ export function ExportCarDialog({
               This feature is in beta. You can read more about repository
               exports in{' '}
               <InlineLink
-                to="https://atproto.com/blog/repo-export"
+                to="https://docs.bsky.app/blog/repo-export"
                 style={[a.text_sm]}>
-                this blogpost.
+                this blogpost
               </InlineLink>
+              .
             </Trans>
           </P>
 
diff --git a/src/view/screens/Settings/index.tsx b/src/view/screens/Settings/index.tsx
index 9abf0f2bd..790ce5ee9 100644
--- a/src/view/screens/Settings/index.tsx
+++ b/src/view/screens/Settings/index.tsx
@@ -3,70 +3,71 @@ import {
   ActivityIndicator,
   Linking,
   Platform,
-  StyleSheet,
   Pressable,
+  StyleSheet,
   TextStyle,
   TouchableOpacity,
   View,
   ViewStyle,
 } from 'react-native'
-import {useFocusEffect, useNavigation} from '@react-navigation/native'
 import {
   FontAwesomeIcon,
   FontAwesomeIconStyle,
 } from '@fortawesome/react-native-fontawesome'
-import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
-import * as AppInfo from 'lib/app-info'
-import {usePalette} from 'lib/hooks/usePalette'
-import {useCustomPalette} from 'lib/hooks/useCustomPalette'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {useAccountSwitcher} from 'lib/hooks/useAccountSwitcher'
-import {useAnalytics} from 'lib/analytics/analytics'
-import {NavigationProp} from 'lib/routes/types'
-import {HandIcon, HashtagIcon} from 'lib/icons'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
 import Clipboard from '@react-native-clipboard/clipboard'
-import {makeProfileLink} from 'lib/routes/links'
-import {RQKEY as RQKEY_PROFILE} from '#/state/queries/profile'
+import {useFocusEffect, useNavigation} from '@react-navigation/native'
+import {useQueryClient} from '@tanstack/react-query'
+
+import {isNative} from '#/platform/detection'
 import {useModalControls} from '#/state/modals'
-import {
-  useSetMinimalShellMode,
-  useThemePrefs,
-  useSetThemePrefs,
-  useOnboardingDispatch,
-} from '#/state/shell'
+import {clearLegacyStorage} from '#/state/persisted/legacy'
+// TODO import {useInviteCodesQuery} from '#/state/queries/invites'
+import {clear as clearStorage} from '#/state/persisted/store'
 import {
   useRequireAltTextEnabled,
   useSetRequireAltTextEnabled,
 } from '#/state/preferences'
-import {useSession, useSessionApi, SessionAccount} from '#/state/session'
-import {useProfileQuery} from '#/state/queries/profile'
-import {useClearPreferencesMutation} from '#/state/queries/preferences'
-// TODO import {useInviteCodesQuery} from '#/state/queries/invites'
-import {clear as clearStorage} from '#/state/persisted/store'
-import {clearLegacyStorage} from '#/state/persisted/legacy'
-import {STATUS_PAGE_URL} from 'lib/constants'
-import {Trans, msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {useQueryClient} from '@tanstack/react-query'
-import {useLoggedOutViewControls} from '#/state/shell/logged-out'
-import {useCloseAllActiveElements} from '#/state/util'
 import {
   useInAppBrowser,
   useSetInAppBrowser,
 } from '#/state/preferences/in-app-browser'
-import {isNative} from '#/platform/detection'
-import {useDialogControl} from '#/components/Dialog'
-
-import {s, colors} from 'lib/styles'
-import {ScrollView} from 'view/com/util/Views'
+import {useClearPreferencesMutation} from '#/state/queries/preferences'
+import {RQKEY as RQKEY_PROFILE} from '#/state/queries/profile'
+import {useProfileQuery} from '#/state/queries/profile'
+import {SessionAccount, useSession, useSessionApi} from '#/state/session'
+import {
+  useOnboardingDispatch,
+  useSetMinimalShellMode,
+  useSetThemePrefs,
+  useThemePrefs,
+} from '#/state/shell'
+import {useLoggedOutViewControls} from '#/state/shell/logged-out'
+import {useCloseAllActiveElements} from '#/state/util'
+import {useAnalytics} from 'lib/analytics/analytics'
+import * as AppInfo from 'lib/app-info'
+import {STATUS_PAGE_URL} from 'lib/constants'
+import {useAccountSwitcher} from 'lib/hooks/useAccountSwitcher'
+import {useCustomPalette} from 'lib/hooks/useCustomPalette'
+import {usePalette} from 'lib/hooks/usePalette'
+import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
+import {HandIcon, HashtagIcon} from 'lib/icons'
+import {makeProfileLink} from 'lib/routes/links'
+import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
+import {NavigationProp} from 'lib/routes/types'
+import {colors, s} from 'lib/styles'
+import {AccountDropdownBtn} from 'view/com/util/AccountDropdownBtn'
+import {SelectableBtn} from 'view/com/util/forms/SelectableBtn'
+import {ToggleButton} from 'view/com/util/forms/ToggleButton'
 import {Link, TextLink} from 'view/com/util/Link'
+import {SimpleViewHeader} from 'view/com/util/SimpleViewHeader'
 import {Text} from 'view/com/util/text/Text'
 import * as Toast from 'view/com/util/Toast'
 import {UserAvatar} from 'view/com/util/UserAvatar'
-import {ToggleButton} from 'view/com/util/forms/ToggleButton'
-import {SelectableBtn} from 'view/com/util/forms/SelectableBtn'
-import {AccountDropdownBtn} from 'view/com/util/AccountDropdownBtn'
-import {SimpleViewHeader} from 'view/com/util/SimpleViewHeader'
+import {ScrollView} from 'view/com/util/Views'
+import {useDialogControl} from '#/components/Dialog'
+import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings'
 import {ExportCarDialog} from './ExportCarDialog'
 
 function SettingsAccountCard({account}: {account: SessionAccount}) {
@@ -81,7 +82,11 @@ function SettingsAccountCard({account}: {account: SessionAccount}) {
   const contents = (
     <View style={[pal.view, styles.linkCard]}>
       <View style={styles.avi}>
-        <UserAvatar size={40} avatar={profile?.avatar} />
+        <UserAvatar
+          size={40}
+          avatar={profile?.avatar}
+          type={profile?.associated?.labeler ? 'labeler' : 'user'}
+        />
       </View>
       <View style={[s.flex1]}>
         <Text type="md-bold" style={pal.text}>
@@ -95,7 +100,9 @@ function SettingsAccountCard({account}: {account: SessionAccount}) {
       {isCurrentAccount ? (
         <TouchableOpacity
           testID="signOutBtn"
-          onPress={logout}
+          onPress={() => {
+            logout('Settings')
+          }}
           accessibilityRole="button"
           accessibilityLabel={_(msg`Sign out`)}
           accessibilityHint={`Signs ${profile?.displayName} out of Bluesky`}>
@@ -124,7 +131,9 @@ function SettingsAccountCard({account}: {account: SessionAccount}) {
       testID={`switchToAccountBtn-${account.handle}`}
       key={account.did}
       onPress={
-        isSwitchingAccounts ? undefined : () => onPressSwitchAccount(account)
+        isSwitchingAccounts
+          ? undefined
+          : () => onPressSwitchAccount(account, 'Settings')
       }
       accessibilityRole="button"
       accessibilityLabel={_(msg`Switch to ${account.handle}`)}
@@ -159,6 +168,7 @@ export function SettingsScreen({}: Props) {
   const {setShowLoggedOut} = useLoggedOutViewControls()
   const closeAllActiveElements = useCloseAllActiveElements()
   const exportCarControl = useDialogControl()
+  const birthdayControl = useDialogControl()
 
   // const primaryBg = useCustomPalette<ViewStyle>({
   //   light: {backgroundColor: colors.blue0},
@@ -241,8 +251,8 @@ export function SettingsScreen({}: Props) {
     Toast.show(_(msg`Copied build version to clipboard`))
   }, [_])
 
-  const openHomeFeedPreferences = React.useCallback(() => {
-    navigation.navigate('PreferencesHomeFeed')
+  const openFollowingFeedPreferences = React.useCallback(() => {
+    navigation.navigate('PreferencesFollowingFeed')
   }, [navigation])
 
   const openThreadsPreferences = React.useCallback(() => {
@@ -261,6 +271,10 @@ export function SettingsScreen({}: Props) {
     navigation.navigate('Debug')
   }, [navigation])
 
+  const onPressDebugModeration = React.useCallback(() => {
+    navigation.navigate('DebugMod')
+  }, [navigation])
+
   const onPressSavedFeeds = React.useCallback(() => {
     navigation.navigate('SavedFeeds')
   }, [navigation])
@@ -269,6 +283,10 @@ export function SettingsScreen({}: Props) {
     Linking.openURL(STATUS_PAGE_URL)
   }, [])
 
+  const onPressBirthday = React.useCallback(() => {
+    birthdayControl.open()
+  }, [birthdayControl])
+
   const clearAllStorage = React.useCallback(async () => {
     await clearStorage()
     Toast.show(_(msg`Storage cleared, you need to restart the app now.`))
@@ -281,6 +299,7 @@ export function SettingsScreen({}: Props) {
   return (
     <View style={s.hContentRegion} testID="settingsScreen">
       <ExportCarDialog control={exportCarControl} />
+      <BirthDateSettingsDialog control={birthdayControl} />
 
       <SimpleViewHeader
         showBackButton={isMobile}
@@ -339,7 +358,7 @@ export function SettingsScreen({}: Props) {
               <Text type="lg-medium" style={pal.text}>
                 <Trans>Birthday:</Trans>{' '}
               </Text>
-              <Link onPress={() => openModal({name: 'birth-date-settings'})}>
+              <Link onPress={onPressBirthday}>
                 <Text type="lg" style={pal.link}>
                   <Trans>Show</Trans>
                 </Text>
@@ -472,20 +491,20 @@ export function SettingsScreen({}: Props) {
               label={_(msg`System`)}
               left
               onSelect={() => setColorMode('system')}
-              accessibilityHint={_(msg`Set color theme to system setting`)}
+              accessibilityHint={_(msg`Sets color theme to system setting`)}
             />
             <SelectableBtn
               selected={colorMode === 'light'}
               label={_(msg`Light`)}
               onSelect={() => setColorMode('light')}
-              accessibilityHint={_(msg`Set color theme to light`)}
+              accessibilityHint={_(msg`Sets color theme to light`)}
             />
             <SelectableBtn
               selected={colorMode === 'dark'}
               label={_(msg`Dark`)}
               right
               onSelect={() => setColorMode('dark')}
-              accessibilityHint={_(msg`Set color theme to dark`)}
+              accessibilityHint={_(msg`Sets color theme to dark`)}
             />
           </View>
         </View>
@@ -504,14 +523,14 @@ export function SettingsScreen({}: Props) {
                   label={_(msg`Dim`)}
                   left
                   onSelect={() => setDarkTheme('dim')}
-                  accessibilityHint={_(msg`Set dark theme to the dim theme`)}
+                  accessibilityHint={_(msg`Sets dark theme to the dim theme`)}
                 />
                 <SelectableBtn
                   selected={darkTheme === 'dark'}
                   label={_(msg`Dark`)}
                   right
                   onSelect={() => setDarkTheme('dark')}
-                  accessibilityHint={_(msg`Set dark theme to the dark theme`)}
+                  accessibilityHint={_(msg`Sets dark theme to the dark theme`)}
                 />
               </View>
             </View>
@@ -529,10 +548,10 @@ export function SettingsScreen({}: Props) {
             pal.view,
             isSwitchingAccounts && styles.dimmed,
           ]}
-          onPress={openHomeFeedPreferences}
+          onPress={openFollowingFeedPreferences}
           accessibilityRole="button"
-          accessibilityHint=""
-          accessibilityLabel={_(msg`Opens the home feed preferences`)}>
+          accessibilityLabel={_(msg`Following feed preferences`)}
+          accessibilityHint={_(msg`Opens the Following feed preferences`)}>
           <View style={[styles.iconContainer, pal.btn]}>
             <FontAwesomeIcon
               icon="sliders"
@@ -540,7 +559,7 @@ export function SettingsScreen({}: Props) {
             />
           </View>
           <Text type="lg" style={pal.text}>
-            <Trans>Home Feed Preferences</Trans>
+            <Trans>Following Feed Preferences</Trans>
           </Text>
         </TouchableOpacity>
         <TouchableOpacity
@@ -552,8 +571,8 @@ export function SettingsScreen({}: Props) {
           ]}
           onPress={openThreadsPreferences}
           accessibilityRole="button"
-          accessibilityHint=""
-          accessibilityLabel={_(msg`Opens the threads preferences`)}>
+          accessibilityLabel={_(msg`Thread preferences`)}
+          accessibilityHint={_(msg`Opens the threads preferences`)}>
           <View style={[styles.iconContainer, pal.btn]}>
             <FontAwesomeIcon
               icon={['far', 'comments']}
@@ -572,9 +591,10 @@ export function SettingsScreen({}: Props) {
             pal.view,
             isSwitchingAccounts && styles.dimmed,
           ]}
-          accessibilityHint="My Saved Feeds"
-          accessibilityLabel={_(msg`Opens screen with all saved feeds`)}
-          onPress={onPressSavedFeeds}>
+          onPress={onPressSavedFeeds}
+          accessibilityRole="button"
+          accessibilityLabel={_(msg`My saved feeds`)}
+          accessibilityHint={_(msg`Opens screen with all saved feeds`)}>
           <View style={[styles.iconContainer, pal.btn]}>
             <HashtagIcon style={pal.text} size={18} strokeWidth={3} />
           </View>
@@ -673,7 +693,7 @@ export function SettingsScreen({}: Props) {
           onPress={onPressAppPasswords}
           accessibilityRole="button"
           accessibilityLabel={_(msg`App password settings`)}
-          accessibilityHint={_(msg`Opens the app password settings page`)}>
+          accessibilityHint={_(msg`Opens the app password settings`)}>
           <View style={[styles.iconContainer, pal.btn]}>
             <FontAwesomeIcon
               icon="lock"
@@ -694,7 +714,9 @@ export function SettingsScreen({}: Props) {
           onPress={isSwitchingAccounts ? undefined : onPressChangeHandle}
           accessibilityRole="button"
           accessibilityLabel={_(msg`Change handle`)}
-          accessibilityHint={_(msg`Choose a new Bluesky username or create`)}>
+          accessibilityHint={_(
+            msg`Opens modal for choosing a new Bluesky handle`,
+          )}>
           <View style={[styles.iconContainer, pal.btn]}>
             <FontAwesomeIcon
               icon="at"
@@ -730,7 +752,9 @@ export function SettingsScreen({}: Props) {
           onPress={() => openModal({name: 'change-password'})}
           accessibilityRole="button"
           accessibilityLabel={_(msg`Change password`)}
-          accessibilityHint={_(msg`Change your Bluesky password`)}>
+          accessibilityHint={_(
+            msg`Opens modal for changing your Bluesky password`,
+          )}>
           <View style={[styles.iconContainer, pal.btn]}>
             <FontAwesomeIcon
               icon="lock"
@@ -752,7 +776,7 @@ export function SettingsScreen({}: Props) {
           accessibilityRole="button"
           accessibilityLabel={_(msg`Export my data`)}
           accessibilityHint={_(
-            msg`Download Bluesky account data (repository)`,
+            msg`Opens modal for downloading your Bluesky account data (repository)`,
           )}>
           <View style={[styles.iconContainer, pal.btn]}>
             <FontAwesomeIcon
@@ -771,7 +795,7 @@ export function SettingsScreen({}: Props) {
           accessibilityRole="button"
           accessibilityLabel={_(msg`Delete account`)}
           accessibilityHint={_(
-            msg`Opens modal for account deletion confirmation. Requires email code.`,
+            msg`Opens modal for account deletion confirmation. Requires email code`,
           )}>
           <View style={[styles.iconContainer, dangerBg]}>
             <FontAwesomeIcon
@@ -789,8 +813,8 @@ export function SettingsScreen({}: Props) {
           style={[pal.view, styles.linkCardNoIcon]}
           onPress={onPressSystemLog}
           accessibilityRole="button"
-          accessibilityHint="Open system log"
-          accessibilityLabel={_(msg`Opens the system log page`)}>
+          accessibilityLabel={_(msg`Open system log`)}
+          accessibilityHint={_(msg`Opens the system log page`)}>
           <Text type="lg" style={pal.text}>
             <Trans>System log</Trans>
           </Text>
@@ -809,9 +833,19 @@ export function SettingsScreen({}: Props) {
             </TouchableOpacity>
             <TouchableOpacity
               style={[pal.view, styles.linkCardNoIcon]}
+              onPress={onPressDebugModeration}
+              accessibilityRole="button"
+              accessibilityLabel={_(msg`Open storybook page`)}
+              accessibilityHint={_(msg`Opens the storybook page`)}>
+              <Text type="lg" style={pal.text}>
+                <Trans>Debug Moderation</Trans>
+              </Text>
+            </TouchableOpacity>
+            <TouchableOpacity
+              style={[pal.view, styles.linkCardNoIcon]}
               onPress={onPressResetPreferences}
               accessibilityRole="button"
-              accessibilityLabel={_(msg`Reset preferences`)}
+              accessibilityLabel={_(msg`Reset preferences state`)}
               accessibilityHint={_(msg`Resets the preferences state`)}>
               <Text type="lg" style={pal.text}>
                 <Trans>Reset preferences state</Trans>
@@ -821,7 +855,7 @@ export function SettingsScreen({}: Props) {
               style={[pal.view, styles.linkCardNoIcon]}
               onPress={onPressResetOnboarding}
               accessibilityRole="button"
-              accessibilityLabel={_(msg`Reset onboarding`)}
+              accessibilityLabel={_(msg`Reset onboarding state`)}
               accessibilityHint={_(msg`Resets the onboarding state`)}>
               <Text type="lg" style={pal.text}>
                 <Trans>Reset onboarding state</Trans>
@@ -832,7 +866,7 @@ export function SettingsScreen({}: Props) {
               onPress={clearAllLegacyStorage}
               accessibilityRole="button"
               accessibilityLabel={_(msg`Clear all legacy storage data`)}
-              accessibilityHint={_(msg`Clear all legacy storage data`)}>
+              accessibilityHint={_(msg`Clears all legacy storage data`)}>
               <Text type="lg" style={pal.text}>
                 <Trans>
                   Clear all legacy storage data (restart after this)
@@ -844,7 +878,7 @@ export function SettingsScreen({}: Props) {
               onPress={clearAllStorage}
               accessibilityRole="button"
               accessibilityLabel={_(msg`Clear all storage data`)}
-              accessibilityHint={_(msg`Clear all storage data`)}>
+              accessibilityHint={_(msg`Clears all storage data`)}>
               <Text type="lg" style={pal.text}>
                 <Trans>Clear all storage data (restart after this)</Trans>
               </Text>
@@ -856,9 +890,7 @@ export function SettingsScreen({}: Props) {
             accessibilityRole="button"
             onPress={onPressBuildInfo}>
             <Text type="sm" style={[styles.buildInfo, pal.textLight]}>
-              <Trans>
-                Build version {AppInfo.appVersion} {AppInfo.updateChannel}
-              </Trans>
+              <Trans>Version {AppInfo.appVersion}</Trans>
             </Text>
           </TouchableOpacity>
           <Text type="sm" style={[pal.textLight]}>
@@ -933,7 +965,7 @@ function EmailConfirmationNotice() {
             ]}
             accessibilityRole="button"
             accessibilityLabel={_(msg`Verify my email`)}
-            accessibilityHint=""
+            accessibilityHint={_(msg`Opens modal for email verification`)}
             onPress={() => openModal({name: 'verify-email'})}>
             <FontAwesomeIcon
               icon="envelope"
diff --git a/src/view/screens/Storybook/Buttons.tsx b/src/view/screens/Storybook/Buttons.tsx
index 320db13ff..ad2fff3f4 100644
--- a/src/view/screens/Storybook/Buttons.tsx
+++ b/src/view/screens/Storybook/Buttons.tsx
@@ -129,6 +129,15 @@ export function Buttons() {
           <ButtonIcon icon={Globe} position="left" />
           <ButtonText>Link out</ButtonText>
         </Button>
+
+        <Button
+          variant="gradient"
+          color="gradient_sky"
+          size="tiny"
+          label="Link out">
+          <ButtonIcon icon={Globe} position="left" />
+          <ButtonText>Link out</ButtonText>
+        </Button>
       </View>
 
       <View style={[a.flex_row, a.gap_md, a.align_start]}>
@@ -149,6 +158,14 @@ export function Buttons() {
           <ButtonIcon icon={ChevronLeft} />
         </Button>
         <Button
+          variant="gradient"
+          color="gradient_sunset"
+          size="tiny"
+          shape="round"
+          label="Link out">
+          <ButtonIcon icon={ChevronLeft} />
+        </Button>
+        <Button
           variant="outline"
           color="primary"
           size="large"
@@ -164,6 +181,14 @@ export function Buttons() {
           label="Link out">
           <ButtonIcon icon={ChevronLeft} />
         </Button>
+        <Button
+          variant="ghost"
+          color="primary"
+          size="tiny"
+          shape="round"
+          label="Link out">
+          <ButtonIcon icon={ChevronLeft} />
+        </Button>
       </View>
 
       <View style={[a.flex_row, a.gap_md, a.align_start]}>
@@ -184,6 +209,14 @@ export function Buttons() {
           <ButtonIcon icon={ChevronLeft} />
         </Button>
         <Button
+          variant="gradient"
+          color="gradient_sunset"
+          size="tiny"
+          shape="square"
+          label="Link out">
+          <ButtonIcon icon={ChevronLeft} />
+        </Button>
+        <Button
           variant="outline"
           color="primary"
           size="large"
@@ -199,6 +232,14 @@ export function Buttons() {
           label="Link out">
           <ButtonIcon icon={ChevronLeft} />
         </Button>
+        <Button
+          variant="ghost"
+          color="primary"
+          size="tiny"
+          shape="square"
+          label="Link out">
+          <ButtonIcon icon={ChevronLeft} />
+        </Button>
       </View>
     </View>
   )
diff --git a/src/view/screens/Storybook/Dialogs.tsx b/src/view/screens/Storybook/Dialogs.tsx
index db568c6bd..c2eaf19ac 100644
--- a/src/view/screens/Storybook/Dialogs.tsx
+++ b/src/view/screens/Storybook/Dialogs.tsx
@@ -9,7 +9,8 @@ import * as Prompt from '#/components/Prompt'
 import {useDialogStateControlContext} from '#/state/dialogs'
 
 export function Dialogs() {
-  const control = Dialog.useDialogControl()
+  const scrollable = Dialog.useDialogControl()
+  const basic = Dialog.useDialogControl()
   const prompt = Prompt.usePromptControl()
   const {closeAllDialogs} = useDialogStateControlContext()
 
@@ -20,8 +21,31 @@ export function Dialogs() {
         color="secondary"
         size="small"
         onPress={() => {
-          control.open()
+          scrollable.open()
           prompt.open()
+          basic.open()
+        }}
+        label="Open basic dialog">
+        Open all dialogs
+      </Button>
+
+      <Button
+        variant="outline"
+        color="secondary"
+        size="small"
+        onPress={() => {
+          scrollable.open()
+        }}
+        label="Open basic dialog">
+        Open scrollable dialog
+      </Button>
+
+      <Button
+        variant="outline"
+        color="secondary"
+        size="small"
+        onPress={() => {
+          basic.open()
         }}
         label="Open basic dialog">
         Open basic dialog
@@ -44,13 +68,22 @@ export function Dialogs() {
         </Prompt.Description>
         <Prompt.Actions>
           <Prompt.Cancel>Cancel</Prompt.Cancel>
-          <Prompt.Action>Confirm</Prompt.Action>
+          <Prompt.Action onPress={() => {}}>Confirm</Prompt.Action>
         </Prompt.Actions>
       </Prompt.Outer>
 
+      <Dialog.Outer control={basic}>
+        <Dialog.Handle />
+
+        <Dialog.Inner label="test">
+          <H3 nativeID="dialog-title">Dialog</H3>
+          <P nativeID="dialog-description">A basic dialog</P>
+        </Dialog.Inner>
+      </Dialog.Outer>
+
       <Dialog.Outer
-        control={control}
-        nativeOptions={{sheet: {snapPoints: ['90%']}}}>
+        control={scrollable}
+        nativeOptions={{sheet: {snapPoints: ['100%']}}}>
         <Dialog.Handle />
 
         <Dialog.ScrollableInner
@@ -77,9 +110,13 @@ export function Dialogs() {
                 variant="outline"
                 color="primary"
                 size="small"
-                onPress={() => control.close()}
+                onPress={() =>
+                  scrollable.close(() => {
+                    console.log('CLOSED')
+                  })
+                }
                 label="Open basic dialog">
-                Close basic dialog
+                Close dialog
               </Button>
             </View>
           </View>
diff --git a/src/view/screens/Storybook/Icons.tsx b/src/view/screens/Storybook/Icons.tsx
index 73466e077..9d7dc0aa8 100644
--- a/src/view/screens/Storybook/Icons.tsx
+++ b/src/view/screens/Storybook/Icons.tsx
@@ -6,6 +6,7 @@ import {H1} from '#/components/Typography'
 import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
 import {ArrowTopRight_Stroke2_Corner0_Rounded as ArrowTopRight} from '#/components/icons/ArrowTopRight'
 import {CalendarDays_Stroke2_Corner0_Rounded as CalendarDays} from '#/components/icons/CalendarDays'
+import {Loader} from '#/components/Loader'
 
 export function Icons() {
   const t = useTheme()
@@ -36,6 +37,14 @@ export function Icons() {
         <CalendarDays size="lg" fill={t.atoms.text.color} />
         <CalendarDays size="xl" fill={t.atoms.text.color} />
       </View>
+
+      <View style={[a.flex_row, a.gap_xl]}>
+        <Loader size="xs" fill={t.atoms.text.color} />
+        <Loader size="sm" fill={t.atoms.text.color} />
+        <Loader size="md" fill={t.atoms.text.color} />
+        <Loader size="lg" fill={t.atoms.text.color} />
+        <Loader size="xl" fill={t.atoms.text.color} />
+      </View>
     </View>
   )
 }
diff --git a/src/view/screens/Storybook/Links.tsx b/src/view/screens/Storybook/Links.tsx
index 2828e74bf..f9ecfba55 100644
--- a/src/view/screens/Storybook/Links.tsx
+++ b/src/view/screens/Storybook/Links.tsx
@@ -4,7 +4,7 @@ import {View} from 'react-native'
 import {useTheme, atoms as a} from '#/alf'
 import {ButtonText} from '#/components/Button'
 import {InlineLink, Link} from '#/components/Link'
-import {H1, H3, Text} from '#/components/Typography'
+import {H1, Text} from '#/components/Typography'
 
 export function Links() {
   const t = useTheme()
@@ -13,26 +13,19 @@ export function Links() {
       <H1>Links</H1>
 
       <View style={[a.gap_md, a.align_start]}>
-        <InlineLink
-          to="https://bsky.social"
-          warnOnMismatchingTextChild
-          style={[a.text_md]}>
-          External
+        <InlineLink to="https://google.com" style={[a.text_lg]}>
+          https://google.com
         </InlineLink>
-        <InlineLink to="https://bsky.social" style={[a.text_md]}>
-          <H3>External with custom children</H3>
+        <InlineLink to="https://google.com" style={[a.text_lg]}>
+          External with custom children (google.com)
         </InlineLink>
         <InlineLink
           to="https://bsky.social"
-          warnOnMismatchingTextChild
-          style={[a.text_lg]}>
-          https://bsky.social
+          style={[a.text_md, t.atoms.text_contrast_low]}>
+          Internal (bsky.social)
         </InlineLink>
-        <InlineLink
-          to="https://bsky.app/profile/bsky.app"
-          warnOnMismatchingTextChild
-          style={[a.text_md]}>
-          Internal
+        <InlineLink to="https://bsky.app/profile/bsky.app" style={[a.text_md]}>
+          Internal (bsky.app)
         </InlineLink>
 
         <Link
diff --git a/src/view/screens/Storybook/Menus.tsx b/src/view/screens/Storybook/Menus.tsx
new file mode 100644
index 000000000..2f2b14721
--- /dev/null
+++ b/src/view/screens/Storybook/Menus.tsx
@@ -0,0 +1,79 @@
+import React from 'react'
+import {View} from 'react-native'
+
+import {atoms as a, useTheme} from '#/alf'
+import {Text} from '#/components/Typography'
+import * as Menu from '#/components/Menu'
+import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2'
+// import {useDialogStateControlContext} from '#/state/dialogs'
+
+export function Menus() {
+  const t = useTheme()
+  const menuControl = Menu.useMenuControl()
+  // const {closeAllDialogs} = useDialogStateControlContext()
+
+  return (
+    <View style={[a.gap_md]}>
+      <View style={[a.flex_row, a.align_start]}>
+        <Menu.Root control={menuControl}>
+          <Menu.Trigger label="Open basic menu">
+            {({state, props}) => {
+              return (
+                <Text
+                  {...props}
+                  style={[
+                    a.py_sm,
+                    a.px_md,
+                    a.rounded_sm,
+                    t.atoms.bg_contrast_50,
+                    (state.hovered || state.focused || state.pressed) && [
+                      t.atoms.bg_contrast_200,
+                    ],
+                  ]}>
+                  Open
+                </Text>
+              )
+            }}
+          </Menu.Trigger>
+
+          <Menu.Outer>
+            <Menu.Group>
+              <Menu.Item label="Click me" onPress={() => {}}>
+                <Menu.ItemIcon icon={Search} />
+                <Menu.ItemText>Click me</Menu.ItemText>
+              </Menu.Item>
+
+              <Menu.Item
+                label="Another item"
+                onPress={() => menuControl.close()}>
+                <Menu.ItemText>Another item</Menu.ItemText>
+              </Menu.Item>
+            </Menu.Group>
+
+            <Menu.Divider />
+
+            <Menu.Group>
+              <Menu.Item label="Click me" onPress={() => {}}>
+                <Menu.ItemIcon icon={Search} />
+                <Menu.ItemText>Click me</Menu.ItemText>
+              </Menu.Item>
+
+              <Menu.Item
+                label="Another item"
+                onPress={() => menuControl.close()}>
+                <Menu.ItemText>Another item</Menu.ItemText>
+              </Menu.Item>
+            </Menu.Group>
+
+            <Menu.Divider />
+
+            <Menu.Item label="Click me" onPress={() => {}}>
+              <Menu.ItemIcon icon={Search} />
+              <Menu.ItemText>Click me</Menu.ItemText>
+            </Menu.Item>
+          </Menu.Outer>
+        </Menu.Root>
+      </View>
+    </View>
+  )
+}
diff --git a/src/view/screens/Storybook/Palette.tsx b/src/view/screens/Storybook/Palette.tsx
index b521fe860..42000aa81 100644
--- a/src/view/screens/Storybook/Palette.tsx
+++ b/src/view/screens/Storybook/Palette.tsx
@@ -1,179 +1,185 @@
 import React from 'react'
 import {View} from 'react-native'
 
-import * as tokens from '#/alf/tokens'
-import {atoms as a} from '#/alf'
+import {atoms as a, useTheme} from '#/alf'
 
 export function Palette() {
+  const t = useTheme()
   return (
     <View style={[a.gap_md]}>
       <View style={[a.flex_row, a.gap_md]}>
-        <View
-          style={[a.flex_1, {height: 60, backgroundColor: tokens.color.gray_0}]}
-        />
-        <View
-          style={[
-            a.flex_1,
-            {height: 60, backgroundColor: tokens.color.gray_25},
-          ]}
-        />
+        <View style={[a.flex_1, t.atoms.bg_contrast_25, {height: 60}]} />
+        <View style={[a.flex_1, t.atoms.bg_contrast_50, {height: 60}]} />
+        <View style={[a.flex_1, t.atoms.bg_contrast_100, {height: 60}]} />
+        <View style={[a.flex_1, t.atoms.bg_contrast_200, {height: 60}]} />
+        <View style={[a.flex_1, t.atoms.bg_contrast_300, {height: 60}]} />
+        <View style={[a.flex_1, t.atoms.bg_contrast_400, {height: 60}]} />
+        <View style={[a.flex_1, t.atoms.bg_contrast_500, {height: 60}]} />
+        <View style={[a.flex_1, t.atoms.bg_contrast_600, {height: 60}]} />
+        <View style={[a.flex_1, t.atoms.bg_contrast_700, {height: 60}]} />
+        <View style={[a.flex_1, t.atoms.bg_contrast_800, {height: 60}]} />
+        <View style={[a.flex_1, t.atoms.bg_contrast_900, {height: 60}]} />
+        <View style={[a.flex_1, t.atoms.bg_contrast_950, {height: 60}]} />
+        <View style={[a.flex_1, t.atoms.bg_contrast_975, {height: 60}]} />
+      </View>
+
+      <View style={[a.flex_row, a.gap_md]}>
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.gray_50},
+            {height: 60, backgroundColor: t.palette.primary_25},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.gray_100},
+            {height: 60, backgroundColor: t.palette.primary_50},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.gray_200},
+            {height: 60, backgroundColor: t.palette.primary_100},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.gray_300},
+            {height: 60, backgroundColor: t.palette.primary_200},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.gray_400},
+            {height: 60, backgroundColor: t.palette.primary_300},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.gray_500},
+            {height: 60, backgroundColor: t.palette.primary_400},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.gray_600},
+            {height: 60, backgroundColor: t.palette.primary_500},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.gray_700},
+            {height: 60, backgroundColor: t.palette.primary_600},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.gray_800},
+            {height: 60, backgroundColor: t.palette.primary_700},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.gray_900},
+            {height: 60, backgroundColor: t.palette.primary_800},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.gray_950},
+            {height: 60, backgroundColor: t.palette.primary_900},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.gray_975},
+            {height: 60, backgroundColor: t.palette.primary_950},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.gray_1000},
+            {height: 60, backgroundColor: t.palette.primary_975},
           ]}
         />
       </View>
-
       <View style={[a.flex_row, a.gap_md]}>
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.blue_25},
+            {height: 60, backgroundColor: t.palette.positive_25},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.blue_50},
+            {height: 60, backgroundColor: t.palette.positive_50},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.blue_100},
+            {height: 60, backgroundColor: t.palette.positive_100},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.blue_200},
+            {height: 60, backgroundColor: t.palette.positive_200},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.blue_300},
+            {height: 60, backgroundColor: t.palette.positive_300},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.blue_400},
+            {height: 60, backgroundColor: t.palette.positive_400},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.blue_500},
+            {height: 60, backgroundColor: t.palette.positive_500},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.blue_600},
+            {height: 60, backgroundColor: t.palette.positive_600},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.blue_700},
+            {height: 60, backgroundColor: t.palette.positive_700},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.blue_800},
+            {height: 60, backgroundColor: t.palette.positive_800},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.blue_900},
+            {height: 60, backgroundColor: t.palette.positive_900},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.blue_950},
+            {height: 60, backgroundColor: t.palette.positive_950},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.blue_975},
+            {height: 60, backgroundColor: t.palette.positive_975},
           ]}
         />
       </View>
@@ -181,153 +187,79 @@ export function Palette() {
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.green_25},
-          ]}
-        />
-        <View
-          style={[
-            a.flex_1,
-            {height: 60, backgroundColor: tokens.color.green_50},
-          ]}
-        />
-        <View
-          style={[
-            a.flex_1,
-            {height: 60, backgroundColor: tokens.color.green_100},
-          ]}
-        />
-        <View
-          style={[
-            a.flex_1,
-            {height: 60, backgroundColor: tokens.color.green_200},
-          ]}
-        />
-        <View
-          style={[
-            a.flex_1,
-            {height: 60, backgroundColor: tokens.color.green_300},
-          ]}
-        />
-        <View
-          style={[
-            a.flex_1,
-            {height: 60, backgroundColor: tokens.color.green_400},
+            {height: 60, backgroundColor: t.palette.negative_25},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.green_500},
+            {height: 60, backgroundColor: t.palette.negative_50},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.green_600},
-          ]}
-        />
-        <View
-          style={[
-            a.flex_1,
-            {height: 60, backgroundColor: tokens.color.green_700},
-          ]}
-        />
-        <View
-          style={[
-            a.flex_1,
-            {height: 60, backgroundColor: tokens.color.green_800},
-          ]}
-        />
-        <View
-          style={[
-            a.flex_1,
-            {height: 60, backgroundColor: tokens.color.green_900},
-          ]}
-        />
-        <View
-          style={[
-            a.flex_1,
-            {height: 60, backgroundColor: tokens.color.green_950},
-          ]}
-        />
-        <View
-          style={[
-            a.flex_1,
-            {height: 60, backgroundColor: tokens.color.green_975},
-          ]}
-        />
-      </View>
-      <View style={[a.flex_row, a.gap_md]}>
-        <View
-          style={[a.flex_1, {height: 60, backgroundColor: tokens.color.red_25}]}
-        />
-        <View
-          style={[a.flex_1, {height: 60, backgroundColor: tokens.color.red_50}]}
-        />
-        <View
-          style={[
-            a.flex_1,
-            {height: 60, backgroundColor: tokens.color.red_100},
+            {height: 60, backgroundColor: t.palette.negative_100},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.red_200},
+            {height: 60, backgroundColor: t.palette.negative_200},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.red_300},
+            {height: 60, backgroundColor: t.palette.negative_300},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.red_400},
+            {height: 60, backgroundColor: t.palette.negative_400},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.red_500},
+            {height: 60, backgroundColor: t.palette.negative_500},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.red_600},
+            {height: 60, backgroundColor: t.palette.negative_600},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.red_700},
+            {height: 60, backgroundColor: t.palette.negative_700},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.red_800},
+            {height: 60, backgroundColor: t.palette.negative_800},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.red_900},
+            {height: 60, backgroundColor: t.palette.negative_900},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.red_950},
+            {height: 60, backgroundColor: t.palette.negative_950},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.red_975},
+            {height: 60, backgroundColor: t.palette.negative_975},
           ]}
         />
       </View>
diff --git a/src/view/screens/Storybook/Typography.tsx b/src/view/screens/Storybook/Typography.tsx
index 5d3a96f4d..f0d67c528 100644
--- a/src/view/screens/Storybook/Typography.tsx
+++ b/src/view/screens/Storybook/Typography.tsx
@@ -8,7 +8,9 @@ import {RichText} from '#/components/RichText'
 export function Typography() {
   return (
     <View style={[a.gap_md]}>
-      <Text style={[a.text_5xl]}>atoms.text_5xl</Text>
+      <Text selectable style={[a.text_5xl]}>
+        atoms.text_5xl
+      </Text>
       <Text style={[a.text_4xl]}>atoms.text_4xl</Text>
       <Text style={[a.text_3xl]}>atoms.text_3xl</Text>
       <Text style={[a.text_2xl]}>atoms.text_2xl</Text>
@@ -20,11 +22,14 @@ export function Typography() {
       <Text style={[a.text_2xs]}>atoms.text_2xs</Text>
 
       <RichText
-        resolveFacets
+        // TODO: This only supports already resolved facets.
+        // Resolving them on read is bad anyway.
         value={`This is rich text. It can have mentions like @bsky.app or links like https://bsky.social`}
       />
       <RichText
-        resolveFacets
+        selectable
+        // TODO: This only supports already resolved facets.
+        // Resolving them on read is bad anyway.
         value={`This is rich text. It can have mentions like @bsky.app or links like https://bsky.social`}
         style={[a.text_xl]}
       />
diff --git a/src/view/screens/Storybook/index.tsx b/src/view/screens/Storybook/index.tsx
index 40929555e..3a2e2f369 100644
--- a/src/view/screens/Storybook/index.tsx
+++ b/src/view/screens/Storybook/index.tsx
@@ -16,6 +16,7 @@ import {Dialogs} from './Dialogs'
 import {Breakpoints} from './Breakpoints'
 import {Shadows} from './Shadows'
 import {Icons} from './Icons'
+import {Menus} from './Menus'
 
 export function Storybook() {
   const t = useTheme()
@@ -66,6 +67,7 @@ export function Storybook() {
             </Button>
           </View>
 
+          <Dialogs />
           <ThemeProvider theme="light">
             <Theming />
           </ThemeProvider>
@@ -84,6 +86,7 @@ export function Storybook() {
           <Links />
           <Forms />
           <Dialogs />
+          <Menus />
           <Breakpoints />
         </View>
       </CenteredView>
diff --git a/src/view/shell/Composer.tsx b/src/view/shell/Composer.tsx
index d37ff4fb7..1937fcb6e 100644
--- a/src/view/shell/Composer.tsx
+++ b/src/view/shell/Composer.tsx
@@ -55,6 +55,8 @@ export const Composer = observer(function ComposerImpl({
         onPost={state.onPost}
         quote={state.quote}
         mention={state.mention}
+        text={state.text}
+        imageUris={state.imageUris}
       />
     </Animated.View>
   )
diff --git a/src/view/shell/Composer.web.tsx b/src/view/shell/Composer.web.tsx
index 99e659d62..00233f66a 100644
--- a/src/view/shell/Composer.web.tsx
+++ b/src/view/shell/Composer.web.tsx
@@ -9,7 +9,7 @@ import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock'
 import {
   EmojiPicker,
   EmojiPickerState,
-} from 'view/com/composer/text-input/web/EmojiPicker.web.tsx'
+} from 'view/com/composer/text-input/web/EmojiPicker.web'
 
 const BOTTOM_BAR_HEIGHT = 61
 
@@ -69,6 +69,7 @@ export function Composer({}: {winHeight: number}) {
           onPost={state.onPost}
           mention={state.mention}
           openPicker={onOpenPicker}
+          text={state.text}
         />
       </Animated.View>
       <EmojiPicker state={pickerState} close={onClosePicker} />
diff --git a/src/view/shell/Drawer.tsx b/src/view/shell/Drawer.tsx
index 2a37d1fe9..1bf5647f6 100644
--- a/src/view/shell/Drawer.tsx
+++ b/src/view/shell/Drawer.tsx
@@ -75,6 +75,7 @@ let DrawerProfileCard = ({
         avatar={profile?.avatar}
         // See https://github.com/bluesky-social/social-app/pull/1801:
         usePlainRNImage={true}
+        type={profile?.associated?.labeler ? 'labeler' : 'user'}
       />
       <Text
         type="title-lg"
@@ -93,10 +94,12 @@ let DrawerProfileCard = ({
           {formatCountShortOnly(profile?.followersCount ?? 0)}
         </Text>{' '}
         {pluralize(profile?.followersCount || 0, 'follower')} &middot;{' '}
-        <Text type="xl-medium" style={pal.text}>
-          {formatCountShortOnly(profile?.followsCount ?? 0)}
-        </Text>{' '}
-        following
+        <Trans>
+          <Text type="xl-medium" style={pal.text}>
+            {formatCountShortOnly(profile?.followsCount ?? 0)}
+          </Text>{' '}
+          following
+        </Trans>
       </Text>
     </TouchableOpacity>
   )
diff --git a/src/view/shell/NavSignupCard.tsx b/src/view/shell/NavSignupCard.tsx
index bae37e838..83d141498 100644
--- a/src/view/shell/NavSignupCard.tsx
+++ b/src/view/shell/NavSignupCard.tsx
@@ -58,7 +58,7 @@ let NavSignupCard = ({}: {}): React.ReactNode => {
           accessibilityHint={_(msg`Sign in`)}
           accessibilityLabel={_(msg`Sign in`)}>
           <Text type="md" style={[pal.text, s.bold]}>
-            Sign in
+            <Trans>Sign in</Trans>
           </Text>
         </Button>
       </View>
diff --git a/src/view/shell/bottom-bar/BottomBar.tsx b/src/view/shell/bottom-bar/BottomBar.tsx
index 1ab3334fa..8a19a0b4f 100644
--- a/src/view/shell/bottom-bar/BottomBar.tsx
+++ b/src/view/shell/bottom-bar/BottomBar.tsx
@@ -36,6 +36,7 @@ import {Button} from '#/view/com/util/forms/Button'
 import {s} from 'lib/styles'
 import {Logo} from '#/view/icons/Logo'
 import {Logotype} from '#/view/icons/Logotype'
+import {useDedupe} from 'lib/hooks/useDedupe'
 
 type TabOptions = 'Home' | 'Search' | 'Notifications' | 'MyProfile' | 'Feeds'
 
@@ -54,6 +55,7 @@ export function BottomBar({navigation}: BottomTabBarProps) {
   const {data: profile} = useProfileQuery({did: currentAccount?.did})
   const {requestSwitchToAccount} = useLoggedOutViewControls()
   const closeAllActiveElements = useCloseAllActiveElements()
+  const dedupe = useDedupe()
 
   const showSignIn = React.useCallback(() => {
     closeAllActiveElements()
@@ -74,12 +76,12 @@ export function BottomBar({navigation}: BottomTabBarProps) {
       if (tabState === TabState.InsideAtRoot) {
         emitSoftReset()
       } else if (tabState === TabState.Inside) {
-        navigation.dispatch(StackActions.popToTop())
+        dedupe(() => navigation.dispatch(StackActions.popToTop()))
       } else {
-        navigation.navigate(`${tab}Tab`)
+        dedupe(() => navigation.navigate(`${tab}Tab`))
       }
     },
-    [track, navigation],
+    [track, navigation, dedupe],
   )
   const onPressHome = React.useCallback(() => onPressTab('Home'), [onPressTab])
   const onPressSearch = React.useCallback(
@@ -227,6 +229,7 @@ export function BottomBar({navigation}: BottomTabBarProps) {
                       size={27}
                       // See https://github.com/bluesky-social/social-app/pull/1801:
                       usePlainRNImage={true}
+                      type={profile?.associated?.labeler ? 'labeler' : 'user'}
                     />
                   </View>
                 ) : (
@@ -236,6 +239,7 @@ export function BottomBar({navigation}: BottomTabBarProps) {
                       size={28}
                       // See https://github.com/bluesky-social/social-app/pull/1801:
                       usePlainRNImage={true}
+                      type={profile?.associated?.labeler ? 'labeler' : 'user'}
                     />
                   </View>
                 )}
diff --git a/src/view/shell/desktop/Feeds.tsx b/src/view/shell/desktop/Feeds.tsx
index c3b1caa35..f447490b3 100644
--- a/src/view/shell/desktop/Feeds.tsx
+++ b/src/view/shell/desktop/Feeds.tsx
@@ -15,7 +15,7 @@ import {emitSoftReset} from '#/state/events'
 export function DesktopFeeds() {
   const pal = usePalette('default')
   const {_} = useLingui()
-  const {feeds: pinnedFeedInfos} = usePinnedFeedsInfos()
+  const {data: pinnedFeedInfos} = usePinnedFeedsInfos()
   const selectedFeed = useSelectedFeed()
   const setSelectedFeed = useSetSelectedFeed()
   const navigation = useNavigation<NavigationProp>()
@@ -25,7 +25,9 @@ export function DesktopFeeds() {
     }
     return getCurrentRoute(state)
   })
-
+  if (!pinnedFeedInfos) {
+    return null
+  }
   return (
     <View style={[styles.container, pal.view]}>
       {pinnedFeedInfos.map(feedInfo => {
diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx
index fbc90bfc6..097ca2fbf 100644
--- a/src/view/shell/desktop/LeftNav.tsx
+++ b/src/view/shell/desktop/LeftNav.tsx
@@ -64,7 +64,11 @@ function ProfileCard() {
       style={[styles.profileCard, !isDesktop && styles.profileCardTablet]}
       title={_(msg`My Profile`)}
       asAnchor>
-      <UserAvatar avatar={profile.avatar} size={size} />
+      <UserAvatar
+        avatar={profile.avatar}
+        size={size}
+        type={profile?.associated?.labeler ? 'labeler' : 'user'}
+      />
     </Link>
   ) : (
     <View style={[styles.profileCard, !isDesktop && styles.profileCardTablet]}>
@@ -200,10 +204,10 @@ function ComposeBtn() {
   const fetchHandle = useFetchHandle()
 
   const getProfileHandle = async () => {
-    const {routes} = getState()
-    const currentRoute = routes[routes.length - 1]
+    const routes = getState()?.routes
+    const currentRoute = routes?.[routes?.length - 1]
 
-    if (currentRoute.name === 'Profile') {
+    if (currentRoute?.name === 'Profile') {
       let handle: string | undefined = (
         currentRoute.params as CommonNavigatorParams['Profile']
       ).name
@@ -391,7 +395,7 @@ export function DesktopLeftNav() {
               <FontAwesomeIcon
                 icon="hand"
                 style={pal.text as FontAwesomeIconStyle}
-                size={isDesktop ? 20 : 26}
+                size={isDesktop ? 23 : 26}
               />
             }
             label={_(msg`Moderation`)}
diff --git a/src/view/shell/desktop/RightNav.tsx b/src/view/shell/desktop/RightNav.tsx
index 264fef194..c1f498724 100644
--- a/src/view/shell/desktop/RightNav.tsx
+++ b/src/view/shell/desktop/RightNav.tsx
@@ -9,14 +9,13 @@ import {FEEDBACK_FORM_URL, HELP_DESK_URL} from 'lib/constants'
 import {s} from 'lib/styles'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {useLingui} from '@lingui/react'
-import {Trans, msg} from '@lingui/macro'
+import {msg} from '@lingui/macro'
 import {useSession} from '#/state/session'
 
 export function DesktopRightNav({routeName}: {routeName: string}) {
   const pal = usePalette('default')
-  const palError = usePalette('error')
   const {_} = useLingui()
-  const {isSandbox, hasSession, currentAccount} = useSession()
+  const {hasSession, currentAccount} = useSession()
 
   const {isTablet} = useWebMediaQueries()
   if (isTablet) {
@@ -49,13 +48,6 @@ export function DesktopRightNav({routeName}: {routeName: string}) {
               paddingTop: hasSession ? 0 : 18,
             },
           ]}>
-          {isSandbox ? (
-            <View style={[palError.view, styles.messageLine, s.p10]}>
-              <Text type="md" style={[palError.text, s.bold]}>
-                <Trans>SANDBOX. Posts and accounts are not permanent.</Trans>
-              </Text>
-            </View>
-          ) : undefined}
           <View style={[{flexWrap: 'wrap'}, s.flexRow]}>
             {hasSession && (
               <>
diff --git a/src/view/shell/desktop/Search.tsx b/src/view/shell/desktop/Search.tsx
index 4a9483733..0c5bd452f 100644
--- a/src/view/shell/desktop/Search.tsx
+++ b/src/view/shell/desktop/Search.tsx
@@ -11,7 +11,7 @@ import {useNavigation, StackActions} from '@react-navigation/native'
 import {
   AppBskyActorDefs,
   moderateProfile,
-  ProfileModeration,
+  ModerationDecision,
 } from '@atproto/api'
 import {Trans, msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
@@ -86,7 +86,7 @@ export function SearchProfileCard({
   moderation,
 }: {
   profile: AppBskyActorDefs.ProfileViewBasic
-  moderation: ProfileModeration
+  moderation: ModerationDecision
 }) {
   const pal = usePalette('default')
 
@@ -111,7 +111,8 @@ export function SearchProfileCard({
         <UserAvatar
           size={40}
           avatar={profile.avatar}
-          moderation={moderation.avatar}
+          moderation={moderation.ui('avatar')}
+          type={profile.associated?.labeler ? 'labeler' : 'user'}
         />
         <View style={{flex: 1}}>
           <Text
@@ -121,7 +122,7 @@ export function SearchProfileCard({
             lineHeight={1.2}>
             {sanitizeDisplayName(
               profile.displayName || sanitizeHandle(profile.handle),
-              moderation.profile,
+              moderation.ui('displayName'),
             )}
           </Text>
           <Text type="md" style={[pal.textLight]} numberOfLines={1}>
diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx
index 6b0cc6808..f29183095 100644
--- a/src/view/shell/index.tsx
+++ b/src/view/shell/index.tsx
@@ -29,6 +29,9 @@ import {useSession} from '#/state/session'
 import {useCloseAnyActiveElement} from '#/state/util'
 import * as notifications from 'lib/notifications/notifications'
 import {Outlet as PortalOutlet} from '#/components/Portal'
+import {MutedWordsDialog} from '#/components/dialogs/MutedWords'
+import {useDialogStateContext} from 'state/dialogs'
+import Animated from 'react-native-reanimated'
 
 function ShellInner() {
   const isDrawerOpen = useIsDrawerOpen()
@@ -52,6 +55,7 @@ function ShellInner() {
   const canGoBack = useNavigationState(state => !isStateAtTabRoot(state))
   const {hasSession, currentAccount} = useSession()
   const closeAnyActiveElement = useCloseAnyActiveElement()
+  const {importantForAccessibility} = useDialogStateContext()
   // start undefined
   const currentAccountDid = React.useRef<string | undefined>(undefined)
 
@@ -79,7 +83,9 @@ function ShellInner() {
 
   return (
     <>
-      <View style={containerPadding}>
+      <Animated.View
+        style={containerPadding}
+        importantForAccessibility={importantForAccessibility}>
         <ErrorBoundary>
           <Drawer
             renderDrawerContent={renderDrawerContent}
@@ -91,11 +97,12 @@ function ShellInner() {
             <TabsNavigator />
           </Drawer>
         </ErrorBoundary>
-      </View>
+      </Animated.View>
       <Composer winHeight={winDim.height} />
       <ModalsContainer />
-      <PortalOutlet />
+      <MutedWordsDialog />
       <Lightbox />
+      <PortalOutlet />
     </>
   )
 }
diff --git a/src/view/shell/index.web.tsx b/src/view/shell/index.web.tsx
index 97c065502..02993ac46 100644
--- a/src/view/shell/index.web.tsx
+++ b/src/view/shell/index.web.tsx
@@ -1,5 +1,9 @@
 import React, {useEffect} from 'react'
 import {View, StyleSheet, TouchableOpacity} from 'react-native'
+import {useNavigation} from '@react-navigation/native'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
 import {ErrorBoundary} from '../com/util/ErrorBoundary'
 import {Lightbox} from '../com/lightbox/Lightbox'
 import {ModalsContainer} from '../com/modals/Modal'
@@ -9,13 +13,12 @@ import {s, colors} from 'lib/styles'
 import {RoutesContainer, FlatNavigator} from '../../Navigation'
 import {DrawerContent} from './Drawer'
 import {useWebMediaQueries} from '../../lib/hooks/useWebMediaQueries'
-import {useNavigation} from '@react-navigation/native'
 import {NavigationProp} from 'lib/routes/types'
-import {t} from '@lingui/macro'
 import {useIsDrawerOpen, useSetDrawerOpen} from '#/state/shell'
 import {useCloseAllActiveElements} from '#/state/util'
 import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock'
 import {Outlet as PortalOutlet} from '#/components/Portal'
+import {MutedWordsDialog} from '#/components/dialogs/MutedWords'
 
 function ShellInner() {
   const isDrawerOpen = useIsDrawerOpen()
@@ -23,6 +26,7 @@ function ShellInner() {
   const {isDesktop} = useWebMediaQueries()
   const navigator = useNavigation<NavigationProp>()
   const closeAllActiveElements = useCloseAllActiveElements()
+  const {_} = useLingui()
 
   useWebBodyScrollLock(isDrawerOpen)
 
@@ -40,14 +44,16 @@ function ShellInner() {
       </ErrorBoundary>
       <Composer winHeight={0} />
       <ModalsContainer />
-      <PortalOutlet />
+      <MutedWordsDialog />
       <Lightbox />
+      <PortalOutlet />
+
       {!isDesktop && isDrawerOpen && (
         <TouchableOpacity
           onPress={() => setDrawerOpen(false)}
           style={styles.drawerMask}
-          accessibilityLabel={t`Close navigation footer`}
-          accessibilityHint={t`Closes bottom navigation bar`}>
+          accessibilityLabel={_(msg`Close navigation footer`)}
+          accessibilityHint={_(msg`Closes bottom navigation bar`)}>
           <View style={styles.drawerContainer}>
             <DrawerContent />
           </View>