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.tsx8
-rw-r--r--src/view/com/auth/SplashScreen.tsx8
-rw-r--r--src/view/com/auth/create/Policies.tsx19
-rw-r--r--src/view/com/auth/create/state.ts4
-rw-r--r--src/view/com/auth/login/ChooseAccountForm.tsx6
-rw-r--r--src/view/com/auth/login/LoginForm.tsx2
-rw-r--r--src/view/com/auth/onboarding/RecommendedFollowsItem.tsx16
-rw-r--r--src/view/com/auth/onboarding/WelcomeMobile.tsx6
-rw-r--r--src/view/com/auth/server-input/index.tsx2
-rw-r--r--src/view/com/composer/Composer.tsx10
-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/select-language/SelectLangBtn.tsx8
-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/lightbox/ImageViewing/components/ImageDefaultHeader.tsx16
-rw-r--r--src/view/com/lightbox/Lightbox.tsx4
-rw-r--r--src/view/com/modals/AppealLabel.tsx139
-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/ContentFilteringSettings.tsx407
-rw-r--r--src/view/com/modals/DeleteAccount.tsx6
-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.tsx16
-rw-r--r--src/view/com/modals/Modal.web.tsx12
-rw-r--r--src/view/com/modals/ModerationDetails.tsx142
-rw-r--r--src/view/com/modals/SwitchAccount.tsx6
-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/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/notifications/FeedItem.tsx17
-rw-r--r--src/view/com/post-thread/PostLikedBy.tsx4
-rw-r--r--src/view/com/post-thread/PostThread.tsx450
-rw-r--r--src/view/com/post-thread/PostThreadItem.tsx100
-rw-r--r--src/view/com/post/Post.tsx39
-rw-r--r--src/view/com/posts/FeedItem.tsx56
-rw-r--r--src/view/com/profile/ProfileCard.tsx96
-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.tsx598
-rw-r--r--src/view/com/profile/ProfileHeaderSuggestedFollows.tsx26
-rw-r--r--src/view/com/profile/ProfileMenu.tsx83
-rw-r--r--src/view/com/util/BottomSheetCustomBackdrop.tsx7
-rw-r--r--src/view/com/util/ErrorBoundary.tsx23
-rw-r--r--src/view/com/util/Link.tsx8
-rw-r--r--src/view/com/util/PostMeta.tsx18
-rw-r--r--src/view/com/util/UserAvatar.tsx33
-rw-r--r--src/view/com/util/UserBanner.tsx12
-rw-r--r--src/view/com/util/forms/PostDropdownBtn.tsx46
-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.tsx3
-rw-r--r--src/view/com/util/post-embeds/QuoteEmbed.tsx129
-rw-r--r--src/view/com/util/post-embeds/index.tsx106
-rw-r--r--src/view/screens/DebugMod.tsx923
-rw-r--r--src/view/screens/LanguageSettings.tsx4
-rw-r--r--src/view/screens/Moderation.tsx304
-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/Profile.tsx253
-rw-r--r--src/view/screens/ProfileFeed.tsx25
-rw-r--r--src/view/screens/ProfileFollowers.tsx2
-rw-r--r--src/view/screens/ProfileFollows.tsx2
-rw-r--r--src/view/screens/ProfileList.tsx20
-rw-r--r--src/view/screens/Search/Search.tsx3
-rw-r--r--src/view/screens/Settings/ExportCarDialog.tsx3
-rw-r--r--src/view/screens/Settings/index.tsx84
-rw-r--r--src/view/screens/Storybook/Buttons.tsx41
-rw-r--r--src/view/screens/Storybook/index.tsx1
-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.tsx2
-rw-r--r--src/view/shell/desktop/LeftNav.tsx6
-rw-r--r--src/view/shell/desktop/Search.tsx9
-rw-r--r--src/view/shell/index.tsx2
-rw-r--r--src/view/shell/index.web.tsx14
89 files changed, 2089 insertions, 3891 deletions
diff --git a/src/view/com/auth/HomeLoggedOutCTA.tsx b/src/view/com/auth/HomeLoggedOutCTA.tsx
index f796d8bae..a5b5bf7ba 100644
--- a/src/view/com/auth/HomeLoggedOutCTA.tsx
+++ b/src/view/com/auth/HomeLoggedOutCTA.tsx
@@ -52,7 +52,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,7 +70,9 @@ 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,
diff --git a/src/view/com/auth/SplashScreen.tsx b/src/view/com/auth/SplashScreen.tsx
index 134ae11f1..f3d783476 100644
--- a/src/view/com/auth/SplashScreen.tsx
+++ b/src/view/com/auth/SplashScreen.tsx
@@ -66,7 +66,9 @@ export const SplashScreen = ({
             onPress={onPressCreateAccount}
             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, styles.btnLabel]}>
               <Trans>Create a new account</Trans>
             </Text>
@@ -77,7 +79,9 @@ export const SplashScreen = ({
             onPress={onPressSignin}
             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]}>
               <Trans>Sign In</Trans>
             </Text>
diff --git a/src/view/com/auth/create/Policies.tsx b/src/view/com/auth/create/Policies.tsx
index 2c7d60818..803e2ad32 100644
--- a/src/view/com/auth/create/Policies.tsx
+++ b/src/view/com/auth/create/Policies.tsx
@@ -9,6 +9,8 @@ import {TextLink} from '../../util/Link'
 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'
 
 type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema
 
@@ -20,6 +22,7 @@ export const Policies = ({
   needsGuardian: boolean
 }) => {
   const pal = usePalette('default')
+  const {_} = useLingui()
   if (!serviceDescription) {
     return <View />
   }
@@ -40,7 +43,9 @@ export const Policies = ({
           />
         </View>
         <Text style={[pal.textLight, s.pl5, s.flex1]}>
-          This service has not provided terms of service or a privacy policy.
+          <Trans>
+            This service has not provided terms of service or a privacy policy.
+          </Trans>
         </Text>
       </View>
     )
@@ -51,7 +56,7 @@ export const Policies = ({
       <TextLink
         key="tos"
         href={tos}
-        text="Terms of Service"
+        text={_(msg`Terms of Service`)}
         style={[pal.link, s.underline]}
       />,
     )
@@ -61,7 +66,7 @@ export const Policies = ({
       <TextLink
         key="pp"
         href={pp}
-        text="Privacy Policy"
+        text={_(msg`Privacy Policy`)}
         style={[pal.link, s.underline]}
       />,
     )
@@ -79,12 +84,14 @@ export const Policies = ({
   return (
     <View style={styles.policies}>
       <Text style={pal.textLight}>
-        By creating an account you agree to the {els}.
+        <Trans>By creating an account you agree to the {els}.</Trans>
       </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.
+          <Trans>
+            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.
+          </Trans>
         </Text>
       )}
     </View>
diff --git a/src/view/com/auth/create/state.ts b/src/view/com/auth/create/state.ts
index 7a727ec0b..840084dcb 100644
--- a/src/view/com/auth/create/state.ts
+++ b/src/view/com/auth/create/state.ts
@@ -12,7 +12,7 @@ import {createFullHandle, validateHandle} from '#/lib/strings/handles'
 import {cleanError} from '#/lib/strings/errors'
 import {useOnboardingDispatch} from '#/state/shell/onboarding'
 import {useSessionApi} from '#/state/session'
-import {DEFAULT_SERVICE, IS_PROD_SERVICE} from '#/lib/constants'
+import {DEFAULT_SERVICE, IS_TEST_USER} from '#/lib/constants'
 import {
   DEFAULT_PROD_FEEDS,
   usePreferencesSetBirthDateMutation,
@@ -147,7 +147,7 @@ export function useSubmitCreateAccount(
             : undefined,
         })
         setBirthDate({birthDate: uiState.birthDate})
-        if (IS_PROD_SERVICE(uiState.serviceUrl)) {
+        if (!IS_TEST_USER(uiState.handle)) {
           setSavedFeeds(DEFAULT_PROD_FEEDS)
         }
       } catch (e: any) {
diff --git a/src/view/com/auth/login/ChooseAccountForm.tsx b/src/view/com/auth/login/ChooseAccountForm.tsx
index 32cd8315d..d3b075fdb 100644
--- a/src/view/com/auth/login/ChooseAccountForm.tsx
+++ b/src/view/com/auth/login/ChooseAccountForm.tsx
@@ -45,7 +45,11 @@ function AccountItem({
       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} />
+          <UserAvatar
+            avatar={profile?.avatar}
+            size={30}
+            type={profile?.associated?.labeler ? 'labeler' : 'user'}
+          />
         </View>
         <Text style={styles.accountText}>
           <Text type="lg-bold" style={pal.text}>
diff --git a/src/view/com/auth/login/LoginForm.tsx b/src/view/com/auth/login/LoginForm.tsx
index fdba9f203..3202d69c5 100644
--- a/src/view/com/auth/login/LoginForm.tsx
+++ b/src/view/com/auth/login/LoginForm.tsx
@@ -207,7 +207,7 @@ export const LoginForm = ({
             testID="loginPasswordInput"
             ref={passwordInputRef}
             style={[pal.text, styles.textInput]}
-            placeholder="Password"
+            placeholder={_(msg`Password`)}
             placeholderTextColor={pal.colors.textLight}
             autoCapitalize="none"
             autoCorrect={false}
diff --git a/src/view/com/auth/onboarding/RecommendedFollowsItem.tsx b/src/view/com/auth/onboarding/RecommendedFollowsItem.tsx
index 5f81a4d65..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
@@ -62,7 +63,7 @@ function ProfileCard({
   moderation,
 }: {
   profile: Shadow<AppBskyActorDefs.ProfileViewBasic>
-  moderation: ProfileModeration
+  moderation: ModerationDecision
   onFollowStateChange: (props: {
     did: string
     following: boolean
@@ -70,6 +71,7 @@ function ProfileCard({
 }) {
   const {track} = useAnalytics()
   const pal = usePalette('default')
+  const {_} = useLingui()
   const [addingMoreSuggestions, setAddingMoreSuggestions] =
     React.useState(false)
   const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(
@@ -113,7 +115,7 @@ function ProfileCard({
           <UserAvatar
             size={40}
             avatar={profile.avatar}
-            moderation={moderation.avatar}
+            moderation={moderation.ui('avatar')}
           />
         </View>
         <View style={styles.layoutContent}>
@@ -124,7 +126,7 @@ 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}>
@@ -136,7 +138,7 @@ 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 32b5a3141..b26ac1dcb 100644
--- a/src/view/com/auth/server-input/index.tsx
+++ b/src/view/com/auth/server-input/index.tsx
@@ -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 97f8e5194..ddb01a8fa 100644
--- a/src/view/com/composer/Composer.tsx
+++ b/src/view/com/composer/Composer.tsx
@@ -39,7 +39,7 @@ 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 {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'
@@ -415,7 +415,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}
@@ -443,7 +447,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}
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/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/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/lightbox/ImageViewing/components/ImageDefaultHeader.tsx b/src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx
index b23dfedc8..3a040b5d0 100644
--- a/src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx
+++ b/src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx
@@ -6,16 +6,17 @@
  *
  */
 import React from 'react'
-import {createHitslop} from 'lib/constants'
 import {
   SafeAreaView,
   TouchableOpacity,
   StyleSheet,
   ViewStyle,
 } from 'react-native'
-import {t} from '@lingui/macro'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {usePalette} from '#/lib/hooks/usePalette'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {createHitslop} from '#/lib/constants'
 
 type Props = {
   onRequestClose: () => void
@@ -24,8 +25,7 @@ type Props = {
 const HIT_SLOP = createHitslop(16)
 
 const ImageDefaultHeader = ({onRequestClose}: Props) => {
-  const pal = usePalette('default')
-
+  const {_} = useLingui()
   return (
     <SafeAreaView style={styles.root}>
       <TouchableOpacity
@@ -33,10 +33,10 @@ const ImageDefaultHeader = ({onRequestClose}: Props) => {
         onPress={onRequestClose}
         hitSlop={HIT_SLOP}
         accessibilityRole="button"
-        accessibilityLabel={t`Close image`}
-        accessibilityHint={t`Closes viewer for header image`}
+        accessibilityLabel={_(msg`Close image`)}
+        accessibilityHint={_(msg`Closes viewer for header image`)}
         onAccessibilityEscape={onRequestClose}>
-        <FontAwesomeIcon icon="close" color={pal.colors.text} size={22} />
+        <FontAwesomeIcon icon="close" color={'#fff'} size={22} />
       </TouchableOpacity>
     </SafeAreaView>
   )
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/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/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/ContentFilteringSettings.tsx b/src/view/com/modals/ContentFilteringSettings.tsx
deleted file mode 100644
index 3c7edcf0d..000000000
--- a/src/view/com/modals/ContentFilteringSettings.tsx
+++ /dev/null
@@ -1,407 +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'
-import {useDialogControl} from '#/components/Dialog'
-import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings'
-
-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 bithdayDialogControl = useDialogControl()
-
-  const onSetAge = React.useCallback(
-    () => bithdayDialogControl.open(),
-    [bithdayDialogControl],
-  )
-
-  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}>
-      <BirthDateSettingsDialog
-        control={bithdayDialogControl}
-        preferences={preferences}
-      />
-      {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..2301e7a66 100644
--- a/src/view/com/modals/DeleteAccount.tsx
+++ b/src/view/com/modals/DeleteAccount.tsx
@@ -173,7 +173,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 +192,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 +228,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 e382e6fab..238cfc502 100644
--- a/src/view/com/modals/Modal.tsx
+++ b/src/view/com/modals/Modal.tsx
@@ -15,16 +15,12 @@ import * as UserAddRemoveListsModal from './UserAddRemoveLists'
 import * as ListAddUserModal from './ListAddRemoveUsers'
 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 ChangeHandleModal from './ChangeHandle'
 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 VerifyEmailModal from './VerifyEmail'
 import * as ChangeEmailModal from './ChangeEmail'
 import * as ChangePasswordModal from './ChangePassword'
@@ -67,12 +63,6 @@ export function ModalsContainer() {
   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,18 +99,12 @@ export function ModalsContainer() {
   } 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 === '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 66ea2311f..7e5d548ac 100644
--- a/src/view/com/modals/Modal.web.tsx
+++ b/src/view/com/modals/Modal.web.tsx
@@ -8,8 +8,6 @@ import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock'
 import {useModals, useModalControls} from '#/state/modals'
 import type {Modal as ModalIface} from '#/state/modals'
 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'
@@ -23,10 +21,8 @@ import * as EditImageModal from './EditImage'
 import * as ChangeHandleModal from './ChangeHandle'
 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 VerifyEmailModal from './VerifyEmail'
 import * as ChangeEmailModal from './ChangeEmail'
 import * as ChangePasswordModal from './ChangePassword'
@@ -78,10 +74,6 @@ function Modal({modal}: {modal: ModalIface}) {
   let element
   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') {
@@ -104,8 +96,6 @@ function Modal({modal}: {modal: ModalIface}) {
     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') {
@@ -114,8 +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 === '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..0658805bd 100644
--- a/src/view/com/modals/SwitchAccount.tsx
+++ b/src/view/com/modals/SwitchAccount.tsx
@@ -45,7 +45,11 @@ function SwitchAccountCard({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} numberOfLines={1}>
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/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/notifications/FeedItem.tsx b/src/view/com/notifications/FeedItem.tsx
index a46870265..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,
         }
       }) || []),
     ]
@@ -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/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 a7ee42a94..8042e7bd5 100644
--- a/src/view/com/post-thread/PostThread.tsx
+++ b/src/view/com/post-thread/PostThread.tsx
@@ -1,25 +1,14 @@
 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 {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
 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 {
@@ -30,21 +19,18 @@ import {
   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, isWeb} from '#/platform/detection'
 import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
+import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
+import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
+import {cleanError} from 'lib/strings/errors'
 
 // FlatList maintainVisibleContentPosition breaks if too many items
 // are prepended. This seems to be an optimal number based on *shrug*.
@@ -58,9 +44,7 @@ const MAINTAIN_VISIBLE_CONTENT_POSITION = {
 
 const TOP_COMPONENT = {_reactKey: '__top_component__'}
 const REPLY_PROMPT = {_reactKey: '__reply__'}
-const CHILD_SPINNER = {_reactKey: '__child_spinner__'}
 const LOAD_MORE = {_reactKey: '__load_more__'}
-const BOTTOM_COMPONENT = {_reactKey: '__bottom_component__'}
 
 type YieldedItem = ThreadPost | ThreadBlocked | ThreadNotFound
 type RowItem =
@@ -68,9 +52,7 @@ type RowItem =
   // TODO: TS doesn't actually enforce it's one of these, it only enforces matching shape.
   | typeof TOP_COMPONENT
   | typeof REPLY_PROMPT
-  | typeof CHILD_SPINNER
   | typeof LOAD_MORE
-  | typeof BOTTOM_COMPONENT
 
 type ThreadSkeletonParts = {
   parents: YieldedItem[]
@@ -78,6 +60,10 @@ type ThreadSkeletonParts = {
   replies: YieldedItem[]
 }
 
+const keyExtractor = (item: RowItem) => {
+  return item._reactKey
+}
+
 export function PostThread({
   uri,
   onCanReply,
@@ -85,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
 
@@ -105,14 +104,22 @@ 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.id === '!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(
@@ -120,62 +127,6 @@ export function PostThread({
         )}: "${rootPostRecord!.text}"`
       : '',
   )
-  useEffect(() => {
-    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 [maxParents, setMaxParents] = React.useState(
-    isWeb ? Infinity : PARENTS_CHUNK_SIZE,
-  )
-  const [maxReplies, setMaxReplies] = React.useState(100)
-  const treeView = React.useMemo(
-    () => !!threadViewPrefs.lab_treeViewEnabled && hasBranchingReplies(thread),
-    [threadViewPrefs, thread],
-  )
 
   // 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.
@@ -183,18 +134,56 @@ function PostThreadLoaded({
   // 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(
-    () =>
-      createThreadSkeleton(
-        sortThread(thread, threadViewPrefs),
-        hasSession,
-        treeView,
-      ),
-    [thread, threadViewPrefs, hasSession, treeView],
-  )
+  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 (error) {
+      onCanReply(false)
+    } else if (rootPost) {
+      onCanReply(!rootPost.viewer?.replyDisabled)
+    }
+  }, [rootPost, onCanReply, error])
 
   // construct content
   const posts = React.useMemo(() => {
+    if (!skeleton) return []
+
     const {parents, highlightedPost, replies} = skeleton
     let arr: RowItem[] = []
     if (highlightedPost.type === 'post') {
@@ -230,17 +219,11 @@ function PostThreadLoaded({
       if (!highlightedPost.post.viewer?.replyDisabled) {
         arr.push(REPLY_PROMPT)
       }
-      if (highlightedPost.ctx.isChildLoading) {
-        arr.push(CHILD_SPINNER)
-      } else {
-        for (let i = 0; i < replies.length; i++) {
-          arr.push(replies[i])
-          if (i === maxReplies) {
-            arr.push(LOAD_MORE)
-            break
-          }
+      for (let i = 0; i < replies.length; i++) {
+        arr.push(replies[i])
+        if (i === maxReplies) {
+          break
         }
-        arr.push(BOTTOM_COMPONENT)
       }
     }
     return arr
@@ -255,7 +238,7 @@ function PostThreadLoaded({
       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,
@@ -279,10 +262,10 @@ function PostThreadLoaded({
   // 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 (maxParents < skeleton.parents.length) {
+    if (skeleton?.parents && maxParents < skeleton.parents.length) {
       needsBumpMaxParents.current = true
     }
-  }, [maxParents, skeleton.parents.length])
+  }, [maxParents, skeleton?.parents])
   const bumpMaxParentsIfNeeded = React.useCallback(() => {
     if (!isNative) {
       return
@@ -295,6 +278,11 @@ function PostThreadLoaded({
   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: RowItem; index: number}) => {
       if (item === TOP_COMPONENT) {
@@ -325,46 +313,6 @@ function PostThreadLoaded({
             </Text>
           </View>
         )
-      } else if (item === LOAD_MORE) {
-        return (
-          <Pressable
-            onPress={() => setMaxReplies(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)
@@ -373,7 +321,9 @@ function PostThreadLoaded({
           ? (posts[index - 1] as ThreadPost)
           : undefined
         const hasUnrevealedParents =
-          index === 0 && maxParents < skeleton.parents.length
+          index === 0 &&
+          skeleton?.parents &&
+          maxParents < skeleton.parents.length
         return (
           <View
             ref={item.ctx.isHighlightedPost ? highlightedPostRef : undefined}
@@ -390,9 +340,9 @@ function PostThreadLoaded({
               showChildReplyLine={item.ctx.showChildReplyLine}
               showParentReplyLine={item.ctx.showParentReplyLine}
               hasPrecedingItem={
-                !!prev?.ctx.showChildReplyLine || hasUnrevealedParents
+                !!prev?.ctx.showChildReplyLine || !!hasUnrevealedParents
               }
-              onPostReply={onRefresh}
+              onPostReply={refetch}
             />
           </View>
         )
@@ -402,142 +352,62 @@ 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,
-      skeleton.parents.length,
-      maxParents,
-      _,
+      refetch,
     ],
   )
 
   return (
-    <List
-      ref={ref}
-      data={posts}
-      keyExtractor={item => item._reactKey}
-      renderItem={renderItem}
-      onContentSizeChange={isNative ? undefined : onContentSizeChangeWeb}
-      onStartReached={onStartReached}
-      onMomentumScrollEnd={onMomentumScrollEnd}
-      onScrollToTop={onScrollToTop}
-      maintainVisibleContentPosition={
-        isNative ? MAINTAIN_VISIBLE_CONTENT_POSITION : undefined
-      }
-      style={s.hContentRegion}
-      // @ts-ignore our .web version only -prf
-      desktopFixedHeight
-      removeClippedSubviews={isAndroid ? false : undefined}
-      windowSize={11}
-    />
-  )
-}
-
-function PostThreadBlocked() {
-  const {_} = useLingui()
-  const pal = usePalette('default')
-  const navigation = useNavigation<NavigationProp>()
-
-  const onPressBack = React.useCallback(() => {
-    if (navigation.canGoBack()) {
-      navigation.goBack()
-    } else {
-      navigation.navigate('Home')
-    }
-  }, [navigation])
-
-  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}
+    <>
+      <ListMaybePlaceholder
+        isLoading={!preferences || !thread}
+        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}
             />
-            <Trans context="action">Back</Trans>
-          </Text>
-        </TouchableOpacity>
-      </View>
-    </CenteredView>
-  )
-}
-
-function PostThreadError({
-  onRefresh,
-  notFound,
-  error,
-}: {
-  onRefresh: () => void
-  notFound: boolean
-  error: Error | null
-}) {
-  const {_} = useLingui()
-  const pal = usePalette('default')
-  const navigation = useNavigation<NavigationProp>()
-
-  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 (
-    <CenteredView>
-      <ErrorMessage message={cleanError(error)} onPressTryAgain={onRefresh} />
-    </CenteredView>
+          }
+          initialNumToRender={initialNumToRender}
+          windowSize={11}
+        />
+      )}
+    </>
   )
 }
 
@@ -557,7 +427,9 @@ function createThreadSkeleton(
   node: ThreadNode,
   hasSession: boolean,
   treeView: boolean,
-): ThreadSkeletonParts {
+): ThreadSkeletonParts | null {
+  if (!node) return null
+
   return {
     parents: Array.from(flattenThreadParents(node, hasSession)),
     highlightedPost: node,
@@ -614,7 +486,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
   }
@@ -628,20 +503,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/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx
index cd746f9a9..6555bdf73 100644
--- a/src/view/com/post-thread/PostThreadItem.tsx
+++ b/src/view/com/post-thread/PostThreadItem.tsx
@@ -5,7 +5,7 @@ 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'
@@ -19,14 +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 {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'
@@ -147,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
@@ -175,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')
@@ -206,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,
       },
@@ -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
@@ -338,18 +312,9 @@ let PostThreadItemLoaded = ({
                 </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
@@ -432,7 +397,8 @@ let PostThreadItemLoaded = ({
           <PostHider
             testID={`postThreadItem-by-${post.author.handle}`}
             href={postHref}
-            moderation={moderation.content}
+            style={[pal.view]}
+            modui={moderation.ui('contentList')}
             iconSize={isThreadedChild ? 26 : 38}
             iconStyles={
               isThreadedChild
@@ -482,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 && (
@@ -508,19 +475,21 @@ 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}>
@@ -542,18 +511,9 @@ 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}
@@ -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"
diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx
index 7e53eb271..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,8 +14,9 @@ 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 '#/components/RichText'
 import {PreviewableUserAvatar} from '../util/UserAvatar'
@@ -93,7 +94,7 @@ function PostInner({
   post: Shadow<AppBskyFeedDefs.PostView>
   record: AppBskyFeedPost.Record
   richText: RichTextAPI
-  moderation: PostModeration
+  moderation: ModerationDecision
   showReplyLine?: boolean
   style?: StyleProp<ViewStyle>
 }) {
@@ -117,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,
       },
@@ -142,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}
@@ -176,11 +175,15 @@ 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
@@ -202,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
diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx
index f3911da60..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,8 +18,9 @@ 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 {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'
@@ -27,13 +28,11 @@ 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'
@@ -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
@@ -100,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
@@ -108,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) {
@@ -131,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,
       },
@@ -148,7 +139,7 @@ let FeedItemInner = ({
       borderColor: pal.colors.border,
       paddingBottom:
         isThreadLastChild || (!isThreadChild && !isThreadParent)
-          ? 6
+          ? 8
           : undefined,
     },
     isThreadChild ? styles.outerSmallTop : undefined,
@@ -229,6 +220,7 @@ let FeedItemInner = ({
                     numberOfLines={1}
                     text={sanitizeDisplayName(
                       reason.by.displayName || sanitizeHandle(reason.by.handle),
+                      moderation.ui('displayName'),
                     )}
                     href={makeProfileLink(reason.by)}
                   />
@@ -246,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
@@ -264,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}
@@ -295,6 +289,7 @@ let FeedItemInner = ({
               </Text>
             </View>
           )}
+          <LabelsOnMyPost post={post} />
           <PostContent
             moderation={moderation}
             richText={richText}
@@ -306,9 +301,6 @@ let FeedItemInner = ({
             record={record}
             richText={richText}
             onPressReply={onPressReply}
-            showAppealLabelItem={
-              post.author.did === currentAccount?.did && isModeratedPost
-            }
             logContext="FeedItem"
           />
         </View>
@@ -324,7 +316,7 @@ let PostContent = ({
   postEmbed,
   postAuthor,
 }: {
-  moderation: PostModeration
+  moderation: ModerationDecision
   richText: RichTextAPI
   postEmbed: AppBskyFeedDefs.PostView['embed']
   postAuthor: AppBskyFeedDefs.PostView['author']
@@ -342,10 +334,10 @@ 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
@@ -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/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx
index 019e6c10e..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
@@ -234,6 +263,7 @@ export function ProfileCardWithFollowBtn({
               <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 17dc5ce1b..000000000
--- a/src/view/com/profile/ProfileHeader.tsx
+++ /dev/null
@@ -1,598 +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 {
-  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} 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 '#/components/RichText'
-import {UserAvatar} from '../util/UserAvatar'
-import {UserBanner} from '../util/UserBanner'
-import {ProfileHeaderAlerts} from '../util/moderation/ProfileHeaderAlerts'
-import {formatCount} from '../util/numeric/format'
-import {Link} from '../util/Link'
-import {ProfileHeaderSuggestedFollows} from './ProfileHeaderSuggestedFollows'
-import {useModalControls} from '#/state/modals'
-import {useLightboxControls, ProfileImageLightbox} from '#/state/lightbox'
-import {
-  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 {sanitizeDisplayName} from 'lib/strings/display-names'
-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'
-import {atoms as a} from '#/alf'
-import {ProfileMenu} from 'view/com/profile/ProfileMenu'
-import * as Prompt from '#/components/Prompt'
-
-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,
-    'ProfileHeader',
-  )
-  const [__, queueUnblock] = useProfileBlockMutationQueue(profile)
-  const unblockPromptControl = Prompt.usePromptControl()
-  const moderation = useMemo(
-    () => moderateProfile(profile, moderationOpts),
-    [profile, moderationOpts],
-  )
-
-  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 unblockAccount = React.useCallback(async () => {
-    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()}`))
-      }
-    }
-  }, [_, queueUnblock, track])
-
-  const isMe = React.useMemo(
-    () => currentAccount?.did === profile.did,
-    [currentAccount, profile],
-  )
-
-  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={() => unblockPromptControl.open()}
-                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}
-          <ProfileMenu profile={profile} />
-        </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" style={[styles.description]}>
-                <RichText
-                  testID="profileHeaderDescription"
-                  style={[a.text_md]}
-                  numberOfLines={15}
-                  value={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>
-      <Prompt.Basic
-        control={unblockPromptControl}
-        title={_(msg`Unblock Account?`)}
-        description={_(
-          msg`The account will be able to interact with you after unblocking.`,
-        )}
-        onConfirm={unblockAccount}
-        confirmButtonCta={
-          profile.viewer?.blocking ? _(msg`Unblock`) : _(msg`Block`)
-        }
-        confirmButtonColor="negative"
-      />
-    </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 8f2c89499..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,6 +171,7 @@ function SuggestedFollow({
 }) {
   const {track} = useAnalytics()
   const pal = usePalette('default')
+  const {_} = useLingui()
   const moderationOpts = useModerationOpts()
   const profile = useProfileShadow(profileUnshadowed)
   const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(
@@ -181,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
@@ -217,7 +221,7 @@ function SuggestedFollow({
         <UserAvatar
           size={60}
           avatar={profile.avatar}
-          moderation={moderation.avatar}
+          moderation={moderation.ui('avatar')}
         />
 
         <View style={{width: '100%', paddingVertical: 12}}>
@@ -227,7 +231,7 @@ function SuggestedFollow({
             numberOfLines={1}>
             {sanitizeDisplayName(
               profile.displayName || sanitizeHandle(profile.handle),
-              moderation.profile,
+              moderation.ui('displayName'),
             )}
           </Text>
           <Text
@@ -239,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
index 0baa4f394..cb0b1d97c 100644
--- a/src/view/com/profile/ProfileMenu.tsx
+++ b/src/view/com/profile/ProfileMenu.tsx
@@ -17,6 +17,7 @@ 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,
@@ -31,6 +32,7 @@ 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'
@@ -47,12 +49,17 @@ let ProfileMenu = ({
   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 [, queueUnfollow] = useProfileFollowMutationQueue(
+  const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(
     profile,
     'ProfileMenu',
   )
@@ -139,6 +146,19 @@ let ProfileMenu = ({
     }
   }, [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 {
@@ -154,11 +174,8 @@ let ProfileMenu = ({
 
   const onPressReportAccount = React.useCallback(() => {
     track('ProfileHeader:ReportAccountButtonClicked')
-    openModal({
-      name: 'report',
-      did: profile.did,
-    })
-  }, [track, openModal, profile])
+    reportDialogControl.open()
+  }, [track, reportDialogControl])
 
   return (
     <EventStopper onKeyDown={false}>
@@ -175,10 +192,9 @@ let ProfileMenu = ({
                     flexDirection: 'row',
                     alignItems: 'center',
                     justifyContent: 'center',
-                    paddingVertical: 7,
+                    paddingVertical: 10,
                     borderRadius: 50,
-                    marginLeft: 6,
-                    paddingHorizontal: 14,
+                    paddingHorizontal: 16,
                   },
                   pal.btn,
                 ]}>
@@ -210,10 +226,38 @@ let ProfileMenu = ({
               <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`)}
@@ -225,18 +269,6 @@ let ProfileMenu = ({
                 </Menu.Item>
                 {!isSelf && (
                   <>
-                    {profile.viewer?.following &&
-                      (profile.viewer.blocking || profile.viewer.blockedBy) && (
-                        <Menu.Item
-                          testID="profileHeaderDropdownUnfollowBtn"
-                          label={_(msg`Unfollow Account`)}
-                          onPress={onPressUnfollowAccount}>
-                          <Menu.ItemText>
-                            <Trans>Unfollow Account</Trans>
-                          </Menu.ItemText>
-                          <Menu.ItemIcon icon={UserMinus} />
-                        </Menu.Item>
-                      )}
                     {!profile.viewer?.blocking &&
                       !profile.viewer?.mutedByList && (
                         <Menu.Item
@@ -299,6 +331,11 @@ let ProfileMenu = ({
         </Menu.Outer>
       </Menu.Root>
 
+      <ReportDialog
+        control={reportDialogControl}
+        params={{type: 'account', did: profile.did}}
+      />
+
       <Prompt.Basic
         control={blockPromptControl}
         title={
@@ -311,6 +348,10 @@ let ProfileMenu = ({
             ? _(
                 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.`,
               )
diff --git a/src/view/com/util/BottomSheetCustomBackdrop.tsx b/src/view/com/util/BottomSheetCustomBackdrop.tsx
index ed5a2f165..ab6570252 100644
--- a/src/view/com/util/BottomSheetCustomBackdrop.tsx
+++ b/src/view/com/util/BottomSheetCustomBackdrop.tsx
@@ -6,12 +6,15 @@ import Animated, {
   interpolate,
   useAnimatedStyle,
 } from 'react-native-reanimated'
-import {t} from '@lingui/macro'
+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/Link.tsx b/src/view/com/util/Link.tsx
index 7468111b5..b6c512b09 100644
--- a/src/view/com/util/Link.tsx
+++ b/src/view/com/util/Link.tsx
@@ -47,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({
@@ -60,6 +61,7 @@ export const Link = memo(function Link({
   accessible,
   anchorNoUnderline,
   navigationAction,
+  onBeforePress,
   ...props
 }: Props) {
   const t = useTheme()
@@ -70,6 +72,7 @@ export const Link = memo(function Link({
 
   const onPress = React.useCallback(
     (e?: Event) => {
+      onBeforePress?.()
       if (typeof href === 'string') {
         return onPressInner(
           closeModal,
@@ -81,7 +84,7 @@ export const Link = memo(function Link({
         )
       }
     },
-    [closeModal, navigation, navigationAction, href, openLink],
+    [closeModal, navigation, navigationAction, href, openLink, onBeforePress],
   )
 
   if (noFeedback) {
@@ -262,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({
@@ -273,6 +277,7 @@ export const TextLinkOnWebOnly = memo(function DesktopWebTextLink({
   numberOfLines,
   lineHeight,
   navigationAction,
+  disableMismatchWarning,
   ...props
 }: TextLinkOnWebOnlyProps) {
   if (isWeb) {
@@ -287,6 +292,7 @@ export const TextLinkOnWebOnly = memo(function DesktopWebTextLink({
         lineHeight={lineHeight}
         title={props.title}
         navigationAction={navigationAction}
+        disableMismatchWarning={disableMismatchWarning}
         {...props}
       />
     )
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/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx
index 413237397..8656c3f51 100644
--- a/src/view/com/util/UserAvatar.tsx
+++ b/src/view/com/util/UserAvatar.tsx
@@ -24,9 +24,9 @@ import {
 } 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} from '#/alf'
+import {useTheme, tokens} from '#/alf'
 
-export type UserAvatarType = 'user' | 'algo' | 'list'
+export type UserAvatarType = 'user' | 'algo' | 'list' | 'labeler'
 
 interface BaseUserAvatarProps {
   type?: UserAvatarType
@@ -101,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"
@@ -134,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,
diff --git a/src/view/com/util/UserBanner.tsx b/src/view/com/util/UserBanner.tsx
index a5ddfee8a..4fb3726cd 100644
--- a/src/view/com/util/UserBanner.tsx
+++ b/src/view/com/util/UserBanner.tsx
@@ -7,7 +7,7 @@ import {msg, Trans} from '@lingui/macro'
 
 import {colors} from 'lib/styles'
 import {useTheme} from 'lib/ThemeContext'
-import {useTheme as useAlfTheme} from '#/alf'
+import {useTheme as useAlfTheme, tokens} from '#/alf'
 import {openCamera, openCropper, openPicker} from '../../../lib/media/picker'
 import {
   usePhotoLibraryPermission,
@@ -26,10 +26,12 @@ import {StreamingLive_Stroke2_Corner0_Rounded as Library} from '#/components/ico
 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
@@ -167,7 +169,10 @@ export function UserBanner({
   ) : (
     <View
       testID="userBannerFallback"
-      style={[styles.bannerImage, styles.defaultBanner]}
+      style={[
+        styles.bannerImage,
+        type === 'labeler' ? styles.labelerBanner : styles.defaultBanner,
+      ]}
     />
   )
 }
@@ -191,4 +196,7 @@ const styles = StyleSheet.create({
   defaultBanner: {
     backgroundColor: '#0070ff',
   },
+  labelerBanner: {
+    backgroundColor: tokens.color.temp_purple,
+  },
 })
diff --git a/src/view/com/util/forms/PostDropdownBtn.tsx b/src/view/com/util/forms/PostDropdownBtn.tsx
index 8fc3d9ea6..70fbb907f 100644
--- a/src/view/com/util/forms/PostDropdownBtn.tsx
+++ b/src/view/com/util/forms/PostDropdownBtn.tsx
@@ -16,7 +16,6 @@ import * as Toast from '../Toast'
 import {EventStopper} from '../EventStopper'
 import {useDialogControl} from '#/components/Dialog'
 import * as Prompt from '#/components/Prompt'
-import {useModalControls} from '#/state/modals'
 import {makeProfileLink} from '#/lib/routes/links'
 import {CommonNavigatorParams} from '#/lib/routes/types'
 import {getCurrentRoute} from 'lib/routes/helpers'
@@ -33,6 +32,7 @@ import {useSession} from '#/state/session'
 import {isWeb} from '#/platform/detection'
 import {richTextToString} from '#/lib/strings/rich-text-helpers'
 import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
+import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog'
 
 import {atoms as a, useTheme as useAlf} from '#/alf'
 import * as Menu from '#/components/Menu'
@@ -45,7 +45,6 @@ import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/
 import {BubbleQuestion_Stroke2_Corner0_Rounded as Translate} from '#/components/icons/Bubble'
 import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning'
 import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
-import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
 
 let PostDropdownBtn = ({
   testID,
@@ -55,7 +54,6 @@ let PostDropdownBtn = ({
   record,
   richText,
   style,
-  showAppealLabelItem,
   hitSlop,
 }: {
   testID: string
@@ -65,7 +63,6 @@ let PostDropdownBtn = ({
   record: AppBskyFeedPost.Record
   richText: RichTextAPI
   style?: StyleProp<ViewStyle>
-  showAppealLabelItem?: boolean
   hitSlop?: PressableProps['hitSlop']
 }): React.ReactNode => {
   const {hasSession, currentAccount} = useSession()
@@ -73,7 +70,6 @@ let PostDropdownBtn = ({
   const alf = useAlf()
   const {_} = useLingui()
   const defaultCtrlColor = theme.palette.default.postCtrl
-  const {openModal} = useModalControls()
   const langPrefs = useLanguagePrefs()
   const mutedThreads = useMutedThreads()
   const toggleThreadMute = useToggleThreadMute()
@@ -83,6 +79,7 @@ let PostDropdownBtn = ({
   const openLink = useOpenLink()
   const navigation = useNavigation()
   const {mutedWordsDialogControl} = useGlobalDialogsControlContext()
+  const reportDialogControl = useReportDialogControl()
   const deletePromptControl = useDialogControl()
   const hidePromptControl = useDialogControl()
   const loggedOutWarningPromptControl = useDialogControl()
@@ -293,13 +290,7 @@ let PostDropdownBtn = ({
               <Menu.Item
                 testID="postDropdownReportBtn"
                 label={_(msg`Report post`)}
-                onPress={() => {
-                  openModal({
-                    name: 'report',
-                    uri: postUri,
-                    cid: postCid,
-                  })
-                }}>
+                onPress={() => reportDialogControl.open()}>
                 <Menu.ItemText>{_(msg`Report post`)}</Menu.ItemText>
                 <Menu.ItemIcon icon={Warning} position="right" />
               </Menu.Item>
@@ -314,28 +305,6 @@ let PostDropdownBtn = ({
                 <Menu.ItemIcon icon={Trash} position="right" />
               </Menu.Item>
             )}
-
-            {showAppealLabelItem && (
-              <>
-                <Menu.Divider />
-
-                <Menu.Item
-                  testID="postDropdownAppealBtn"
-                  label={_(msg`Appeal content warning`)}
-                  onPress={() => {
-                    openModal({
-                      name: 'appeal-label',
-                      uri: postUri,
-                      cid: postCid,
-                    })
-                  }}>
-                  <Menu.ItemText>
-                    {_(msg`Appeal content warning`)}
-                  </Menu.ItemText>
-                  <Menu.ItemIcon icon={CircleInfo} position="right" />
-                </Menu.Item>
-              </>
-            )}
           </Menu.Group>
         </Menu.Outer>
       </Menu.Root>
@@ -359,6 +328,15 @@ let PostDropdownBtn = ({
         confirmButtonCta={_(msg`Hide`)}
       />
 
+      <ReportDialog
+        control={reportDialogControl}
+        params={{
+          type: 'post',
+          uri: postUri,
+          cid: postCid,
+        }}
+      />
+
       <Prompt.Basic
         control={loggedOutWarningPromptControl}
         title={_(msg`Note about sharing`)}
diff --git a/src/view/com/util/moderation/ContentHider.tsx b/src/view/com/util/moderation/ContentHider.tsx
deleted file mode 100644
index cd2545290..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 = ['muted', 'muted-word'].includes(moderation.cause?.type || '')
-  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 ede62e988..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 = ['muted', 'muted-word'].includes(moderation.cause?.type || '')
-  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 c96954a11..3fa347a6d 100644
--- a/src/view/com/util/post-ctrls/PostCtrls.tsx
+++ b/src/view/com/util/post-ctrls/PostCtrls.tsx
@@ -41,7 +41,6 @@ let PostCtrls = ({
   post,
   record,
   richText,
-  showAppealLabelItem,
   style,
   onPressReply,
   logContext,
@@ -50,7 +49,6 @@ let PostCtrls = ({
   post: Shadow<AppBskyFeedDefs.PostView>
   record: AppBskyFeedPost.Record
   richText: RichTextAPI
-  showAppealLabelItem?: boolean
   style?: StyleProp<ViewStyle>
   onPressReply: () => void
   logContext: 'FeedItem' | 'PostThreadItem' | 'Post'
@@ -232,7 +230,6 @@ let PostCtrls = ({
           postUri={post.uri}
           record={record}
           richText={richText}
-          showAppealLabelItem={showAppealLabelItem}
           style={styles.btnPad}
           hitSlop={big ? HITSLOP_20 : HITSLOP_10}
         />
diff --git a/src/view/com/util/post-embeds/QuoteEmbed.tsx b/src/view/com/util/post-embeds/QuoteEmbed.tsx
index 35b091269..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,20 +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 {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')
@@ -39,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}
       />
     )
@@ -75,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()
@@ -95,6 +119,7 @@ export function QuoteEmbed({
         : undefined,
     [quote.text, quote.facets],
   )
+
   const embed = React.useMemo(() => {
     const e = quote.embeds?.[0]
 
@@ -108,40 +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
-          enableTags
-          value={richText}
-          style={[a.text_md]}
-          numberOfLines={20}
-          disableLinks
-          authorHandle={quote.author.handle}
-        />
-      ) : 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/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/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 928766c30..000000000
--- a/src/view/screens/Moderation.tsx
+++ /dev/null
@@ -1,304 +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'
-import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
-
-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()
-  const {mutedWordsDialogControl} = useGlobalDialogsControlContext()
-
-  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=""
-          accessibilityLabel={_(msg`Open content filtering settings`)}>
-          <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>
-        <TouchableOpacity
-          testID="mutedWordsBtn"
-          style={[styles.linkCard, pal.view]}
-          onPress={() => mutedWordsDialogControl.open()}
-          accessibilityRole="tab"
-          accessibilityHint=""
-          accessibilityLabel={_(msg`Open muted words settings`)}>
-          <View style={[styles.iconContainer, pal.btn]}>
-            <FontAwesomeIcon
-              icon="filter"
-              style={pal.text as FontAwesomeIconStyle}
-            />
-          </View>
-          <Text type="lg" style={pal.text}>
-            <Trans>Muted words & tags</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/Profile.tsx b/src/view/screens/Profile.tsx
index b30b4491b..d5a46c5c9 100644
--- a/src/view/screens/Profile.tsx
+++ b/src/view/screens/Profile.tsx
@@ -1,5 +1,5 @@
 import React, {useMemo} from 'react'
-import {StyleSheet, View} from 'react-native'
+import {StyleSheet} from 'react-native'
 import {useFocusEffect} from '@react-navigation/native'
 import {
   AppBskyActorDefs,
@@ -7,48 +7,39 @@ import {
   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 {ScreenHider} from '#/components/moderation/ScreenHider'
 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 {resetProfilePostsQueries} from '#/state/queries/post-feed'
 import {useResolveDidQuery} from '#/state/queries/resolve-uri'
 import {useProfileQuery} from '#/state/queries/profile'
 import {useProfileShadow} from '#/state/cache/profile-shadow'
 import {useSession, getAgent} from '#/state/session'
 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 {useLabelerInfoQuery} from '#/state/queries/labeler'
 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 {ProfileFeedSection} from '#/screens/Profile/Sections/Feed'
+import {ProfileLabelsSection} from '#/screens/Profile/Sections/Labels'
+import {ProfileHeader, ProfileHeaderLoading} from '#/screens/Profile/Header'
+
 interface SectionRef {
   scrollToTop: () => void
 }
@@ -148,16 +139,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 +170,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 +252,15 @@ function ProfileScreenLoaded({
         listsSectionRef.current?.scrollToTop()
       }
     },
-    [postsIndex, repliesIndex, mediaIndex, likesIndex, feedsIndex, listsIndex],
+    [
+      filtersIndex,
+      postsIndex,
+      repliesIndex,
+      mediaIndex,
+      likesIndex,
+      feedsIndex,
+      listsIndex,
+    ],
   )
 
   useFocusEffect(
@@ -278,6 +316,7 @@ function ProfileScreenLoaded({
     return (
       <ProfileHeader
         profile={profile}
+        labeler={labelerInfo}
         descriptionRT={hasDescription ? descriptionRT : null}
         moderationOpts={moderationOpts}
         hideBackButton={hideBackButton}
@@ -286,6 +325,7 @@ function ProfileScreenLoaded({
     )
   }, [
     profile,
+    labelerInfo,
     descriptionRT,
     hasDescription,
     moderationOpts,
@@ -297,8 +337,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 +346,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 +394,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 +429,7 @@ function ProfileScreenLoaded({
               />
             )
           : null}
-        {showListsTab
+        {showListsTab && !profile.associated?.labeler
           ? ({headerHeight, isFocused, scrollElRef}) => (
               <ProfileLists
                 ref={listsSectionRef}
@@ -387,77 +455,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}))
diff --git a/src/view/screens/ProfileFeed.tsx b/src/view/screens/ProfileFeed.tsx
index b3a7328c1..8eeeb5d90 100644
--- a/src/view/screens/ProfileFeed.tsx
+++ b/src/view/screens/ProfileFeed.tsx
@@ -35,7 +35,7 @@ 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 {
@@ -108,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}>
@@ -155,7 +155,7 @@ export function ProfileFeedScreenInner({
   const {_} = useLingui()
   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)
@@ -253,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) => {
@@ -400,6 +395,14 @@ export function ProfileFeedScreenInner({
 
   return (
     <View style={s.hContentRegion}>
+      <ReportDialog
+        control={reportDialogControl}
+        params={{
+          type: 'feedgen',
+          uri: feedInfo.uri,
+          cid: feedInfo.cid,
+        }}
+      />
       <PagerWithHeader
         items={SECTION_TITLES}
         isHeaderReady={true}
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 95046e5b4..58b89f239 100644
--- a/src/view/screens/ProfileList.tsx
+++ b/src/view/screens/ProfileList.tsx
@@ -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,
@@ -236,6 +237,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
   const {_} = useLingui()
   const navigation = useNavigation<NavigationProp>()
   const {currentAccount} = useSession()
+  const reportDialogControl = useReportDialogControl()
   const {openModal} = useModalControls()
   const listMuteMutation = useListMuteMutation()
   const listBlockMutation = useListBlockMutation()
@@ -370,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}`)
@@ -550,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'}
@@ -907,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 42eec53d3..d39f37ed7 100644
--- a/src/view/screens/Search/Search.tsx
+++ b/src/view/screens/Search/Search.tsx
@@ -141,6 +141,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)
             }
           }),
@@ -772,7 +773,7 @@ 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}>
diff --git a/src/view/screens/Settings/ExportCarDialog.tsx b/src/view/screens/Settings/ExportCarDialog.tsx
index dca51c0dc..ba8fad2df 100644
--- a/src/view/screens/Settings/ExportCarDialog.tsx
+++ b/src/view/screens/Settings/ExportCarDialog.tsx
@@ -78,8 +78,9 @@ export function ExportCarDialog({
               <InlineLink
                 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 3b5e190c1..7e808f910 100644
--- a/src/view/screens/Settings/index.tsx
+++ b/src/view/screens/Settings/index.tsx
@@ -40,10 +40,7 @@ import {
 } from '#/state/preferences'
 import {useSession, useSessionApi, SessionAccount} from '#/state/session'
 import {useProfileQuery} from '#/state/queries/profile'
-import {
-  useClearPreferencesMutation,
-  usePreferencesQuery,
-} from '#/state/queries/preferences'
+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'
@@ -85,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}>
@@ -156,7 +157,6 @@ export function SettingsScreen({}: Props) {
   const {screen, track} = useAnalytics()
   const {openModal} = useModalControls()
   const {isSwitchingAccounts, accounts, currentAccount} = useSession()
-  const {data: preferences} = usePreferencesQuery()
   const {mutate: clearPreferences} = useClearPreferencesMutation()
   // TODO
   // const {data: invites} = useInviteCodesQuery()
@@ -267,6 +267,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])
@@ -291,10 +295,7 @@ export function SettingsScreen({}: Props) {
   return (
     <View style={s.hContentRegion} testID="settingsScreen">
       <ExportCarDialog control={exportCarControl} />
-      <BirthDateSettingsDialog
-        control={birthdayControl}
-        preferences={preferences}
-      />
+      <BirthDateSettingsDialog control={birthdayControl} />
 
       <SimpleViewHeader
         showBackButton={isMobile}
@@ -486,20 +487,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>
@@ -518,14 +519,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>
@@ -545,8 +546,8 @@ export function SettingsScreen({}: Props) {
           ]}
           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"
@@ -566,8 +567,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']}
@@ -586,9 +587,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>
@@ -687,7 +689,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"
@@ -708,7 +710,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"
@@ -744,7 +748,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"
@@ -766,7 +772,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
@@ -785,7 +791,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
@@ -803,8 +809,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>
@@ -823,9 +829,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>
@@ -835,7 +851,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>
@@ -846,7 +862,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)
@@ -858,7 +874,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>
@@ -947,7 +963,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/index.tsx b/src/view/screens/Storybook/index.tsx
index e43d756de..3a2e2f369 100644
--- a/src/view/screens/Storybook/index.tsx
+++ b/src/view/screens/Storybook/index.tsx
@@ -67,6 +67,7 @@ export function Storybook() {
             </Button>
           </View>
 
+          <Dialogs />
           <ThemeProvider theme="light">
             <Theming />
           </ThemeProvider>
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 115faa296..8a19a0b4f 100644
--- a/src/view/shell/bottom-bar/BottomBar.tsx
+++ b/src/view/shell/bottom-bar/BottomBar.tsx
@@ -229,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>
                 ) : (
@@ -238,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/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx
index c56ba941e..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]}>
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 76a7f8fb3..f29183095 100644
--- a/src/view/shell/index.tsx
+++ b/src/view/shell/index.tsx
@@ -101,8 +101,8 @@ function ShellInner() {
       <Composer winHeight={winDim.height} />
       <ModalsContainer />
       <MutedWordsDialog />
-      <PortalOutlet />
       <Lightbox />
+      <PortalOutlet />
     </>
   )
 }
diff --git a/src/view/shell/index.web.tsx b/src/view/shell/index.web.tsx
index 71dccb8c4..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,9 +13,7 @@ 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'
@@ -24,6 +26,7 @@ function ShellInner() {
   const {isDesktop} = useWebMediaQueries()
   const navigator = useNavigation<NavigationProp>()
   const closeAllActiveElements = useCloseAllActiveElements()
+  const {_} = useLingui()
 
   useWebBodyScrollLock(isDrawerOpen)
 
@@ -42,14 +45,15 @@ function ShellInner() {
       <Composer winHeight={0} />
       <ModalsContainer />
       <MutedWordsDialog />
-      <PortalOutlet />
       <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>