about summary refs log tree commit diff
path: root/src/view/com
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/com')
-rw-r--r--src/view/com/auth/create/state.ts4
-rw-r--r--src/view/com/auth/onboarding/RecommendedFollowsItem.tsx10
-rw-r--r--src/view/com/composer/Composer.tsx2
-rw-r--r--src/view/com/composer/ComposerReplyTo.tsx6
-rw-r--r--src/view/com/composer/select-language/SelectLangBtn.tsx8
-rw-r--r--src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx37
-rw-r--r--src/view/com/modals/AppealLabel.tsx139
-rw-r--r--src/view/com/modals/BirthDateSettings.tsx151
-rw-r--r--src/view/com/modals/ContentFilteringSettings.tsx401
-rw-r--r--src/view/com/modals/Modal.tsx20
-rw-r--r--src/view/com/modals/Modal.web.tsx15
-rw-r--r--src/view/com/modals/ModerationDetails.tsx142
-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.tsx10
-rw-r--r--src/view/com/post-thread/PostLikedBy.tsx4
-rw-r--r--src/view/com/post-thread/PostThread.tsx11
-rw-r--r--src/view/com/post-thread/PostThreadItem.tsx90
-rw-r--r--src/view/com/post/Post.tsx32
-rw-r--r--src/view/com/posts/FeedItem.tsx49
-rw-r--r--src/view/com/profile/ProfileCard.tsx91
-rw-r--r--src/view/com/profile/ProfileHeader.tsx598
-rw-r--r--src/view/com/profile/ProfileHeaderSuggestedFollows.tsx4
-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.tsx10
-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
43 files changed, 451 insertions, 3033 deletions
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/onboarding/RecommendedFollowsItem.tsx b/src/view/com/auth/onboarding/RecommendedFollowsItem.tsx
index 5f81a4d65..434b10c22 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'
@@ -18,7 +18,7 @@ import {logger} from '#/logger'
 
 type Props = {
   profile: AppBskyActorDefs.ProfileViewBasic
-  moderation: ProfileModeration
+  moderation: ModerationDecision
   onFollowStateChange: (props: {
     did: string
     following: boolean
@@ -62,7 +62,7 @@ function ProfileCard({
   moderation,
 }: {
   profile: Shadow<AppBskyActorDefs.ProfileViewBasic>
-  moderation: ProfileModeration
+  moderation: ModerationDecision
   onFollowStateChange: (props: {
     did: string
     following: boolean
@@ -113,7 +113,7 @@ function ProfileCard({
           <UserAvatar
             size={40}
             avatar={profile.avatar}
-            moderation={moderation.avatar}
+            moderation={moderation.ui('avatar')}
           />
         </View>
         <View style={styles.layoutContent}>
@@ -124,7 +124,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}>
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx
index 97f8e5194..0a2692d06 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'
diff --git a/src/view/com/composer/ComposerReplyTo.tsx b/src/view/com/composer/ComposerReplyTo.tsx
index 39a1473a3..4832bca02 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,7 @@ export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) {
       <UserAvatar
         avatar={replyTo.author.avatar}
         size={50}
-        moderation={replyTo.moderation?.avatar}
+        moderation={replyTo.moderation?.ui('avatar')}
       />
       <View style={styles.replyToPost}>
         <Text type="xl-medium" style={[pal.text]}>
@@ -103,7 +103,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/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/lightbox/ImageViewing/components/ImageDefaultHeader.tsx b/src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx
index 3401adaff..3872919de 100644
--- a/src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx
+++ b/src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx
@@ -6,9 +6,11 @@
  *
  */
 import React from 'react'
-import {createHitslop} from 'lib/constants'
 import {SafeAreaView, Text, TouchableOpacity, StyleSheet} from 'react-native'
-import {t} from '@lingui/macro'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {createHitslop} from '#/lib/constants'
 
 type Props = {
   onRequestClose: () => void
@@ -16,20 +18,23 @@ type Props = {
 
 const HIT_SLOP = createHitslop(16)
 
-const ImageDefaultHeader = ({onRequestClose}: Props) => (
-  <SafeAreaView style={styles.root}>
-    <TouchableOpacity
-      style={styles.closeButton}
-      onPress={onRequestClose}
-      hitSlop={HIT_SLOP}
-      accessibilityRole="button"
-      accessibilityLabel={t`Close image`}
-      accessibilityHint={t`Closes viewer for header image`}
-      onAccessibilityEscape={onRequestClose}>
-      <Text style={styles.closeText}>✕</Text>
-    </TouchableOpacity>
-  </SafeAreaView>
-)
+const ImageDefaultHeader = ({onRequestClose}: Props) => {
+  const {_} = useLingui()
+  return (
+    <SafeAreaView style={styles.root}>
+      <TouchableOpacity
+        style={styles.closeButton}
+        onPress={onRequestClose}
+        hitSlop={HIT_SLOP}
+        accessibilityRole="button"
+        accessibilityLabel={_(msg`Close image`)}
+        accessibilityHint={_(msg`Closes viewer for header image`)}
+        onAccessibilityEscape={onRequestClose}>
+        <Text style={styles.closeText}>✕</Text>
+      </TouchableOpacity>
+    </SafeAreaView>
+  )
+}
 
 const styles = StyleSheet.create({
   root: {
diff --git a/src/view/com/modals/AppealLabel.tsx b/src/view/com/modals/AppealLabel.tsx
deleted file mode 100644
index b0aaaf625..000000000
--- a/src/view/com/modals/AppealLabel.tsx
+++ /dev/null
@@ -1,139 +0,0 @@
-import React, {useState} from 'react'
-import {StyleSheet, TouchableOpacity, View} from 'react-native'
-import {ComAtprotoModerationDefs} from '@atproto/api'
-import {ScrollView, TextInput} from './util'
-import {Text} from '../util/text/Text'
-import {s, colors} from 'lib/styles'
-import {usePalette} from 'lib/hooks/usePalette'
-import {Trans, msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {useModalControls} from '#/state/modals'
-import {CharProgress} from '../composer/char-progress/CharProgress'
-import {getAgent} from '#/state/session'
-import * as Toast from '../util/Toast'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
-
-export const snapPoints = ['40%']
-
-type ReportComponentProps =
-  | {
-      uri: string
-      cid: string
-    }
-  | {
-      did: string
-    }
-
-export function Component(props: ReportComponentProps) {
-  const pal = usePalette('default')
-  const [details, setDetails] = useState<string>('')
-  const {_} = useLingui()
-  const {closeModal} = useModalControls()
-  const {isMobile} = useWebMediaQueries()
-  const isAccountReport = 'did' in props
-
-  const submit = async () => {
-    try {
-      const $type = !isAccountReport
-        ? 'com.atproto.repo.strongRef'
-        : 'com.atproto.admin.defs#repoRef'
-      await getAgent().createModerationReport({
-        reasonType: ComAtprotoModerationDefs.REASONAPPEAL,
-        subject: {
-          $type,
-          ...props,
-        },
-        reason: details,
-      })
-      Toast.show(_(msg`We'll look into your appeal promptly.`))
-    } finally {
-      closeModal()
-    }
-  }
-
-  return (
-    <View
-      style={[
-        pal.view,
-        s.flex1,
-        isMobile ? {paddingHorizontal: 12} : undefined,
-      ]}
-      testID="appealLabelModal">
-      <Text
-        type="2xl-bold"
-        style={[pal.text, s.textCenter, {paddingBottom: 8}]}>
-        <Trans>Appeal Content Warning</Trans>
-      </Text>
-      <ScrollView>
-        <View style={[pal.btn, styles.detailsInputContainer]}>
-          <TextInput
-            accessibilityLabel={_(msg`Text input field`)}
-            accessibilityHint={_(
-              msg`Please tell us why you think this content warning was incorrectly applied!`,
-            )}
-            placeholder={_(
-              msg`Please tell us why you think this content warning was incorrectly applied!`,
-            )}
-            placeholderTextColor={pal.textLight.color}
-            value={details}
-            onChangeText={setDetails}
-            autoFocus={true}
-            numberOfLines={3}
-            multiline={true}
-            textAlignVertical="top"
-            maxLength={300}
-            style={[styles.detailsInput, pal.text]}
-          />
-          <View style={styles.detailsInputBottomBar}>
-            <View style={styles.charCounter}>
-              <CharProgress count={details?.length || 0} />
-            </View>
-          </View>
-        </View>
-        <TouchableOpacity
-          testID="confirmBtn"
-          onPress={submit}
-          style={styles.btn}
-          accessibilityRole="button"
-          accessibilityLabel={_(msg`Confirm`)}
-          accessibilityHint="">
-          <Text style={[s.white, s.bold, s.f18]}>
-            <Trans>Submit</Trans>
-          </Text>
-        </TouchableOpacity>
-      </ScrollView>
-    </View>
-  )
-}
-
-const styles = StyleSheet.create({
-  detailsInputContainer: {
-    borderRadius: 8,
-    marginBottom: 8,
-  },
-  detailsInput: {
-    paddingHorizontal: 12,
-    paddingTop: 12,
-    paddingBottom: 12,
-    borderRadius: 8,
-    minHeight: 100,
-    fontSize: 16,
-  },
-  detailsInputBottomBar: {
-    alignSelf: 'flex-end',
-  },
-  charCounter: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    paddingRight: 10,
-    paddingBottom: 8,
-  },
-  btn: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    justifyContent: 'center',
-    borderRadius: 32,
-    padding: 14,
-    backgroundColor: colors.blue3,
-  },
-})
diff --git a/src/view/com/modals/BirthDateSettings.tsx b/src/view/com/modals/BirthDateSettings.tsx
deleted file mode 100644
index 1cab95989..000000000
--- a/src/view/com/modals/BirthDateSettings.tsx
+++ /dev/null
@@ -1,151 +0,0 @@
-import React, {useState} from 'react'
-import {
-  ActivityIndicator,
-  StyleSheet,
-  TouchableOpacity,
-  View,
-} from 'react-native'
-import {Text} from '../util/text/Text'
-import {DateInput} from '../util/forms/DateInput'
-import {ErrorMessage} from '../util/error/ErrorMessage'
-import {s, colors} from 'lib/styles'
-import {usePalette} from 'lib/hooks/usePalette'
-import {isWeb} from 'platform/detection'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {cleanError} from 'lib/strings/errors'
-import {Trans, msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {useModalControls} from '#/state/modals'
-import {
-  usePreferencesQuery,
-  usePreferencesSetBirthDateMutation,
-  UsePreferencesQueryResponse,
-} from '#/state/queries/preferences'
-import {logger} from '#/logger'
-
-export const snapPoints = ['50%', '90%']
-
-function Inner({preferences}: {preferences: UsePreferencesQueryResponse}) {
-  const pal = usePalette('default')
-  const {isMobile} = useWebMediaQueries()
-  const {_} = useLingui()
-  const {
-    isPending,
-    isError,
-    error,
-    mutateAsync: setBirthDate,
-  } = usePreferencesSetBirthDateMutation()
-  const [date, setDate] = useState(preferences.birthDate || new Date())
-  const {closeModal} = useModalControls()
-
-  const onSave = React.useCallback(async () => {
-    try {
-      await setBirthDate({birthDate: date})
-      closeModal()
-    } catch (e) {
-      logger.error(`setBirthDate failed`, {message: e})
-    }
-  }, [date, setBirthDate, closeModal])
-
-  return (
-    <View
-      testID="birthDateSettingsModal"
-      style={[pal.view, styles.container, isMobile && {paddingHorizontal: 18}]}>
-      <View style={styles.titleSection}>
-        <Text type="title-lg" style={[pal.text, styles.title]}>
-          <Trans>My Birthday</Trans>
-        </Text>
-      </View>
-
-      <Text type="lg" style={[pal.textLight, {marginBottom: 10}]}>
-        <Trans>This information is not shared with other users.</Trans>
-      </Text>
-
-      <View>
-        <DateInput
-          handleAsUTC
-          testID="birthdayInput"
-          value={date}
-          onChange={setDate}
-          buttonType="default-light"
-          buttonStyle={[pal.border, styles.dateInputButton]}
-          buttonLabelType="lg"
-          accessibilityLabel={_(msg`Birthday`)}
-          accessibilityHint={_(msg`Enter your birth date`)}
-          accessibilityLabelledBy="birthDate"
-        />
-      </View>
-
-      {isError ? (
-        <ErrorMessage message={cleanError(error)} style={styles.error} />
-      ) : undefined}
-
-      <View style={[styles.btnContainer, pal.borderDark]}>
-        {isPending ? (
-          <View style={styles.btn}>
-            <ActivityIndicator color="#fff" />
-          </View>
-        ) : (
-          <TouchableOpacity
-            testID="confirmBtn"
-            onPress={onSave}
-            style={styles.btn}
-            accessibilityRole="button"
-            accessibilityLabel={_(msg`Save`)}
-            accessibilityHint="">
-            <Text style={[s.white, s.bold, s.f18]}>
-              <Trans>Save</Trans>
-            </Text>
-          </TouchableOpacity>
-        )}
-      </View>
-    </View>
-  )
-}
-
-export function Component({}: {}) {
-  const {data: preferences} = usePreferencesQuery()
-
-  return !preferences ? (
-    <ActivityIndicator />
-  ) : (
-    <Inner preferences={preferences} />
-  )
-}
-
-const styles = StyleSheet.create({
-  container: {
-    flex: 1,
-    paddingBottom: isWeb ? 0 : 40,
-  },
-  titleSection: {
-    paddingTop: isWeb ? 0 : 4,
-    paddingBottom: isWeb ? 14 : 10,
-  },
-  title: {
-    textAlign: 'center',
-    fontWeight: '600',
-    marginBottom: 5,
-  },
-  error: {
-    borderRadius: 6,
-    marginTop: 10,
-  },
-  dateInputButton: {
-    borderWidth: 1,
-    borderRadius: 6,
-    paddingVertical: 14,
-  },
-  btn: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    justifyContent: 'center',
-    borderRadius: 32,
-    padding: 14,
-    backgroundColor: colors.blue3,
-  },
-  btnContainer: {
-    paddingTop: 20,
-    paddingHorizontal: 20,
-  },
-})
diff --git a/src/view/com/modals/ContentFilteringSettings.tsx b/src/view/com/modals/ContentFilteringSettings.tsx
deleted file mode 100644
index 328d23dc2..000000000
--- a/src/view/com/modals/ContentFilteringSettings.tsx
+++ /dev/null
@@ -1,401 +0,0 @@
-import React from 'react'
-import {LabelPreference} from '@atproto/api'
-import {StyleSheet, Pressable, View, Linking} from 'react-native'
-import LinearGradient from 'react-native-linear-gradient'
-import {ScrollView} from './util'
-import {s, colors, gradients} from 'lib/styles'
-import {Text} from '../util/text/Text'
-import {TextLink} from '../util/Link'
-import {ToggleButton} from '../util/forms/ToggleButton'
-import {Button} from '../util/forms/Button'
-import {usePalette} from 'lib/hooks/usePalette'
-import {isIOS} from 'platform/detection'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import * as Toast from '../util/Toast'
-import {logger} from '#/logger'
-import {Trans, msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {useModalControls} from '#/state/modals'
-import {
-  usePreferencesQuery,
-  usePreferencesSetContentLabelMutation,
-  usePreferencesSetAdultContentMutation,
-  ConfigurableLabelGroup,
-  CONFIGURABLE_LABEL_GROUPS,
-  UsePreferencesQueryResponse,
-} from '#/state/queries/preferences'
-
-export const snapPoints = ['90%']
-
-export function Component({}: {}) {
-  const {isMobile} = useWebMediaQueries()
-  const pal = usePalette('default')
-  const {_} = useLingui()
-  const {closeModal} = useModalControls()
-  const {data: preferences} = usePreferencesQuery()
-
-  const onPressDone = React.useCallback(() => {
-    closeModal()
-  }, [closeModal])
-
-  return (
-    <View testID="contentFilteringModal" style={[pal.view, styles.container]}>
-      <Text style={[pal.text, styles.title]}>
-        <Trans>Content Filtering</Trans>
-      </Text>
-
-      <ScrollView style={styles.scrollContainer}>
-        <AdultContentEnabledPref />
-        <ContentLabelPref
-          preferences={preferences}
-          labelGroup="nsfw"
-          disabled={!preferences?.adultContentEnabled}
-        />
-        <ContentLabelPref
-          preferences={preferences}
-          labelGroup="nudity"
-          disabled={!preferences?.adultContentEnabled}
-        />
-        <ContentLabelPref
-          preferences={preferences}
-          labelGroup="suggestive"
-          disabled={!preferences?.adultContentEnabled}
-        />
-        <ContentLabelPref
-          preferences={preferences}
-          labelGroup="gore"
-          disabled={!preferences?.adultContentEnabled}
-        />
-        <ContentLabelPref preferences={preferences} labelGroup="hate" />
-        <ContentLabelPref preferences={preferences} labelGroup="spam" />
-        <ContentLabelPref
-          preferences={preferences}
-          labelGroup="impersonation"
-        />
-        <View style={{height: isMobile ? 60 : 0}} />
-      </ScrollView>
-
-      <View
-        style={[
-          styles.btnContainer,
-          isMobile && styles.btnContainerMobile,
-          pal.borderDark,
-        ]}>
-        <Pressable
-          testID="sendReportBtn"
-          onPress={onPressDone}
-          accessibilityRole="button"
-          accessibilityLabel={_(msg`Done`)}
-          accessibilityHint="">
-          <LinearGradient
-            colors={[gradients.blueLight.start, gradients.blueLight.end]}
-            start={{x: 0, y: 0}}
-            end={{x: 1, y: 1}}
-            style={[styles.btn]}>
-            <Text style={[s.white, s.bold, s.f18]}>
-              <Trans>Done</Trans>
-            </Text>
-          </LinearGradient>
-        </Pressable>
-      </View>
-    </View>
-  )
-}
-
-function AdultContentEnabledPref() {
-  const pal = usePalette('default')
-  const {_} = useLingui()
-  const {data: preferences} = usePreferencesQuery()
-  const {mutate, variables} = usePreferencesSetAdultContentMutation()
-  const {openModal} = useModalControls()
-
-  const onSetAge = React.useCallback(
-    () => openModal({name: 'birth-date-settings'}),
-    [openModal],
-  )
-
-  const onToggleAdultContent = React.useCallback(async () => {
-    if (isIOS) return
-
-    try {
-      mutate({
-        enabled: !(variables?.enabled ?? preferences?.adultContentEnabled),
-      })
-    } catch (e) {
-      Toast.show(
-        _(msg`There was an issue syncing your preferences with the server`),
-      )
-      logger.error('Failed to update preferences with server', {message: e})
-    }
-  }, [variables, preferences, mutate, _])
-
-  const onAdultContentLinkPress = React.useCallback(() => {
-    Linking.openURL('https://bsky.app/')
-  }, [])
-
-  return (
-    <View style={s.mb10}>
-      {isIOS ? (
-        preferences?.adultContentEnabled ? null : (
-          <Text type="md" style={pal.textLight}>
-            <Trans>
-              Adult content can only be enabled via the Web at{' '}
-              <TextLink
-                style={pal.link}
-                href=""
-                text="bsky.app"
-                onPress={onAdultContentLinkPress}
-              />
-              .
-            </Trans>
-          </Text>
-        )
-      ) : typeof preferences?.birthDate === 'undefined' ? (
-        <View style={[pal.viewLight, styles.agePrompt]}>
-          <Text type="md" style={[pal.text, {flex: 1}]}>
-            <Trans>Confirm your age to enable adult content.</Trans>
-          </Text>
-          <Button
-            type="primary"
-            label={_(msg({message: 'Set Age', context: 'action'}))}
-            onPress={onSetAge}
-          />
-        </View>
-      ) : (preferences.userAge || 0) >= 18 ? (
-        <ToggleButton
-          type="default-light"
-          label={_(msg`Enable Adult Content`)}
-          isSelected={variables?.enabled ?? preferences?.adultContentEnabled}
-          onPress={onToggleAdultContent}
-          style={styles.toggleBtn}
-        />
-      ) : (
-        <View style={[pal.viewLight, styles.agePrompt]}>
-          <Text type="md" style={[pal.text, {flex: 1}]}>
-            <Trans>You must be 18 or older to enable adult content.</Trans>
-          </Text>
-          <Button
-            type="primary"
-            label={_(msg({message: 'Set Age', context: 'action'}))}
-            onPress={onSetAge}
-          />
-        </View>
-      )}
-    </View>
-  )
-}
-
-// TODO: Refactor this component to pass labels down to each tab
-function ContentLabelPref({
-  preferences,
-  labelGroup,
-  disabled,
-}: {
-  preferences?: UsePreferencesQueryResponse
-  labelGroup: ConfigurableLabelGroup
-  disabled?: boolean
-}) {
-  const pal = usePalette('default')
-  const visibility = preferences?.contentLabels?.[labelGroup]
-  const {mutate, variables} = usePreferencesSetContentLabelMutation()
-
-  const onChange = React.useCallback(
-    (vis: LabelPreference) => {
-      mutate({labelGroup, visibility: vis})
-    },
-    [mutate, labelGroup],
-  )
-
-  return (
-    <View style={[styles.contentLabelPref, pal.border]}>
-      <View style={s.flex1}>
-        <Text type="md-medium" style={[pal.text]}>
-          {CONFIGURABLE_LABEL_GROUPS[labelGroup].title}
-        </Text>
-        {typeof CONFIGURABLE_LABEL_GROUPS[labelGroup].subtitle === 'string' && (
-          <Text type="sm" style={[pal.textLight]}>
-            {CONFIGURABLE_LABEL_GROUPS[labelGroup].subtitle}
-          </Text>
-        )}
-      </View>
-
-      {disabled || !visibility ? (
-        <Text type="sm-bold" style={pal.textLight}>
-          <Trans context="action">Hide</Trans>
-        </Text>
-      ) : (
-        <SelectGroup
-          current={variables?.visibility || visibility}
-          onChange={onChange}
-          labelGroup={labelGroup}
-        />
-      )}
-    </View>
-  )
-}
-
-interface SelectGroupProps {
-  current: LabelPreference
-  onChange: (v: LabelPreference) => void
-  labelGroup: ConfigurableLabelGroup
-}
-
-function SelectGroup({current, onChange, labelGroup}: SelectGroupProps) {
-  const {_} = useLingui()
-
-  return (
-    <View style={styles.selectableBtns}>
-      <SelectableBtn
-        current={current}
-        value="hide"
-        label={_(msg`Hide`)}
-        left
-        onChange={onChange}
-        labelGroup={labelGroup}
-      />
-      <SelectableBtn
-        current={current}
-        value="warn"
-        label={_(msg`Warn`)}
-        onChange={onChange}
-        labelGroup={labelGroup}
-      />
-      <SelectableBtn
-        current={current}
-        value="ignore"
-        label={_(msg`Show`)}
-        right
-        onChange={onChange}
-        labelGroup={labelGroup}
-      />
-    </View>
-  )
-}
-
-interface SelectableBtnProps {
-  current: string
-  value: LabelPreference
-  label: string
-  left?: boolean
-  right?: boolean
-  onChange: (v: LabelPreference) => void
-  labelGroup: ConfigurableLabelGroup
-}
-
-function SelectableBtn({
-  current,
-  value,
-  label,
-  left,
-  right,
-  onChange,
-  labelGroup,
-}: SelectableBtnProps) {
-  const pal = usePalette('default')
-  const palPrimary = usePalette('inverted')
-  const {_} = useLingui()
-
-  return (
-    <Pressable
-      style={[
-        styles.selectableBtn,
-        left && styles.selectableBtnLeft,
-        right && styles.selectableBtnRight,
-        pal.border,
-        current === value ? palPrimary.view : pal.view,
-      ]}
-      onPress={() => onChange(value)}
-      accessibilityRole="button"
-      accessibilityLabel={value}
-      accessibilityHint={_(
-        msg`Set ${value} for ${labelGroup} content moderation policy`,
-      )}>
-      <Text style={current === value ? palPrimary.text : pal.text}>
-        {label}
-      </Text>
-    </Pressable>
-  )
-}
-
-const styles = StyleSheet.create({
-  container: {
-    flex: 1,
-  },
-  title: {
-    textAlign: 'center',
-    fontWeight: 'bold',
-    fontSize: 24,
-    marginBottom: 12,
-  },
-  description: {
-    paddingHorizontal: 2,
-    marginBottom: 10,
-  },
-  scrollContainer: {
-    flex: 1,
-    paddingHorizontal: 10,
-  },
-  btnContainer: {
-    paddingTop: 10,
-    paddingHorizontal: 10,
-  },
-  btnContainerMobile: {
-    paddingBottom: 40,
-    borderTopWidth: 1,
-  },
-
-  agePrompt: {
-    flexDirection: 'row',
-    justifyContent: 'space-between',
-    alignItems: 'center',
-    paddingLeft: 14,
-    paddingRight: 10,
-    paddingVertical: 8,
-    borderRadius: 8,
-  },
-
-  contentLabelPref: {
-    flexDirection: 'row',
-    justifyContent: 'space-between',
-    alignItems: 'center',
-    paddingTop: 14,
-    paddingLeft: 4,
-    marginBottom: 14,
-    borderTopWidth: 1,
-  },
-
-  selectableBtns: {
-    flexDirection: 'row',
-    marginLeft: 10,
-  },
-  selectableBtn: {
-    flexDirection: 'row',
-    justifyContent: 'center',
-    borderWidth: 1,
-    borderLeftWidth: 0,
-    paddingHorizontal: 10,
-    paddingVertical: 10,
-  },
-  selectableBtnLeft: {
-    borderTopLeftRadius: 8,
-    borderBottomLeftRadius: 8,
-    borderLeftWidth: 1,
-  },
-  selectableBtnRight: {
-    borderTopRightRadius: 8,
-    borderBottomRightRadius: 8,
-  },
-
-  btn: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    justifyContent: 'center',
-    width: '100%',
-    borderRadius: 32,
-    padding: 14,
-    backgroundColor: colors.gray1,
-  },
-  toggleBtn: {
-    paddingHorizontal: 0,
-  },
-})
diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx
index e03879c1d..238cfc502 100644
--- a/src/view/com/modals/Modal.tsx
+++ b/src/view/com/modals/Modal.tsx
@@ -15,17 +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 BirthDateSettingsModal from './BirthDateSettings'
 import * as VerifyEmailModal from './VerifyEmail'
 import * as ChangeEmailModal from './ChangeEmail'
 import * as ChangePasswordModal from './ChangePassword'
@@ -68,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} />
@@ -110,21 +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 === 'birth-date-settings') {
-    snapPoints = BirthDateSettingsModal.snapPoints
-    element = <BirthDateSettingsModal.Component />
   } else if (activeModal?.name === 'verify-email') {
     snapPoints = VerifyEmailModal.snapPoints
     element = <VerifyEmailModal.Component {...activeModal} />
diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx
index d72b7e485..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,11 +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 BirthDateSettingsModal from './BirthDateSettings'
 import * as VerifyEmailModal from './VerifyEmail'
 import * as ChangeEmailModal from './ChangeEmail'
 import * as ChangePasswordModal from './ChangePassword'
@@ -79,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') {
@@ -105,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') {
@@ -115,10 +104,6 @@ function Modal({modal}: {modal: ModalIface}) {
     element = <AltTextImageModal.Component {...modal} />
   } else if (modal.name === 'edit-image') {
     element = <EditImageModal.Component {...modal} />
-  } else if (modal.name === 'moderation-details') {
-    element = <ModerationDetailsModal.Component {...modal} />
-  } else if (modal.name === 'birth-date-settings') {
-    element = <BirthDateSettingsModal.Component />
   } else if (modal.name === 'verify-email') {
     element = <VerifyEmailModal.Component {...modal} />
   } else if (modal.name === 'change-email') {
diff --git a/src/view/com/modals/ModerationDetails.tsx b/src/view/com/modals/ModerationDetails.tsx
deleted file mode 100644
index f890d50dc..000000000
--- a/src/view/com/modals/ModerationDetails.tsx
+++ /dev/null
@@ -1,142 +0,0 @@
-import React from 'react'
-import {StyleSheet, View} from 'react-native'
-import {ModerationUI} from '@atproto/api'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {s} from 'lib/styles'
-import {Text} from '../util/text/Text'
-import {TextLink} from '../util/Link'
-import {usePalette} from 'lib/hooks/usePalette'
-import {isWeb} from 'platform/detection'
-import {listUriToHref} from 'lib/strings/url-helpers'
-import {Button} from '../util/forms/Button'
-import {useModalControls} from '#/state/modals'
-import {useLingui} from '@lingui/react'
-import {Trans, msg} from '@lingui/macro'
-
-export const snapPoints = [300]
-
-export function Component({
-  context,
-  moderation,
-}: {
-  context: 'account' | 'content'
-  moderation: ModerationUI
-}) {
-  const {closeModal} = useModalControls()
-  const {isMobile} = useWebMediaQueries()
-  const pal = usePalette('default')
-  const {_} = useLingui()
-
-  let name
-  let description
-  if (!moderation.cause) {
-    name = _(msg`Content Warning`)
-    description = _(
-      msg`Moderator has chosen to set a general warning on the content.`,
-    )
-  } else if (moderation.cause.type === 'blocking') {
-    if (moderation.cause.source.type === 'list') {
-      const list = moderation.cause.source.list
-      name = _(msg`User Blocked by List`)
-      description = (
-        <Trans>
-          This user is included in the{' '}
-          <TextLink
-            type="2xl"
-            href={listUriToHref(list.uri)}
-            text={list.name}
-            style={pal.link}
-          />{' '}
-          list which you have blocked.
-        </Trans>
-      )
-    } else {
-      name = _(msg`User Blocked`)
-      description = _(
-        msg`You have blocked this user. You cannot view their content.`,
-      )
-    }
-  } else if (moderation.cause.type === 'blocked-by') {
-    name = _(msg`User Blocks You`)
-    description = _(
-      msg`This user has blocked you. You cannot view their content.`,
-    )
-  } else if (moderation.cause.type === 'block-other') {
-    name = _(msg`Content Not Available`)
-    description = _(
-      msg`This content is not available because one of the users involved has blocked the other.`,
-    )
-  } else if (moderation.cause.type === 'muted') {
-    if (moderation.cause.source.type === 'list') {
-      const list = moderation.cause.source.list
-      name = _(msg`Account Muted by List`)
-      description = (
-        <Trans>
-          This user is included in the{' '}
-          <TextLink
-            type="2xl"
-            href={listUriToHref(list.uri)}
-            text={list.name}
-            style={pal.link}
-          />{' '}
-          list which you have muted.
-        </Trans>
-      )
-    } else {
-      name = _(msg`Account Muted`)
-      description = _(msg`You have muted this user.`)
-    }
-  } else {
-    name = moderation.cause.labelDef.strings[context].en.name
-    description = moderation.cause.labelDef.strings[context].en.description
-  }
-
-  return (
-    <View
-      testID="moderationDetailsModal"
-      style={[
-        styles.container,
-        {
-          paddingHorizontal: isMobile ? 14 : 0,
-        },
-        pal.view,
-      ]}>
-      <Text type="title-xl" style={[pal.text, styles.title]}>
-        {name}
-      </Text>
-      <Text type="2xl" style={[pal.text, styles.description]}>
-        {description}
-      </Text>
-      <View style={s.flex1} />
-      <Button
-        type="primary"
-        style={styles.btn}
-        onPress={() => {
-          closeModal()
-        }}>
-        <Text type="button-lg" style={[pal.textLight, s.textCenter, s.white]}>
-          Okay
-        </Text>
-      </Button>
-    </View>
-  )
-}
-
-const styles = StyleSheet.create({
-  container: {
-    flex: 1,
-  },
-  title: {
-    textAlign: 'center',
-    fontWeight: 'bold',
-    marginBottom: 12,
-  },
-  description: {
-    textAlign: 'center',
-  },
-  btn: {
-    paddingVertical: 14,
-    marginTop: isWeb ? 40 : 0,
-    marginBottom: isWeb ? 0 : 40,
-  },
-})
diff --git a/src/view/com/modals/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..b16554790 100644
--- a/src/view/com/notifications/FeedItem.tsx
+++ b/src/view/com/notifications/FeedItem.tsx
@@ -11,7 +11,7 @@ import {
   AppBskyFeedDefs,
   AppBskyFeedPost,
   ModerationOpts,
-  ProfileModeration,
+  ModerationDecision,
   moderateProfile,
   AppBskyEmbedRecordWithMedia,
 } from '@atproto/api'
@@ -54,7 +54,7 @@ interface Author {
   handle: string
   displayName?: string
   avatar?: string
-  moderation: ProfileModeration
+  moderation: ModerationDecision
 }
 
 let FeedItem = ({
@@ -336,7 +336,7 @@ 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')}
         />
       </View>
     )
@@ -354,7 +354,7 @@ function CondensedAuthorsList({
             <UserAvatar
               size={35}
               avatar={author.avatar}
-              moderation={author.moderation.avatar}
+              moderation={author.moderation.ui('avatar')}
             />
           </View>
         ))}
@@ -412,7 +412,7 @@ function ExpandedAuthorsList({
             <UserAvatar
               size={35}
               avatar={author.avatar}
-              moderation={author.moderation.avatar}
+              moderation={author.moderation.ui('avatar')}
             />
           </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..bac7018c3 100644
--- a/src/view/com/post-thread/PostThread.tsx
+++ b/src/view/com/post-thread/PostThread.tsx
@@ -106,11 +106,12 @@ export function PostThread({
         ? 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])
 
   useSetTitle(
diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx
index cd746f9a9..c073b55a0 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')
@@ -256,7 +255,7 @@ let PostThreadItemLoaded = ({
                 did={post.author.did}
                 handle={post.author.handle}
                 avatar={post.author.avatar}
-                moderation={moderation.avatar}
+                moderation={moderation.ui('avatar')}
               />
             </View>
             <View style={styles.layoutContent}>
@@ -271,35 +270,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 +288,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 +315,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 +400,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 +451,7 @@ let PostThreadItemLoaded = ({
                     did={post.author.did}
                     handle={post.author.handle}
                     avatar={post.author.avatar}
-                    moderation={moderation.avatar}
+                    moderation={moderation.ui('avatar')}
                   />
 
                   {showChildReplyLine && (
@@ -508,19 +477,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 +513,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}
diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx
index 7e53eb271..c7bd4ba2f 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>
 }) {
@@ -142,12 +143,13 @@ function PostInner({
             did={post.author.did}
             handle={post.author.handle}
             avatar={post.author.avatar}
-            moderation={moderation.avatar}
+            moderation={moderation.ui('avatar')}
           />
         </View>
         <View style={styles.layoutContent}>
           <PostMeta
             author={post.author}
+            moderation={moderation}
             authorHasWarning={!!post.author.labels?.length}
             timestamp={post.indexedAt}
             postHref={itemHref}
@@ -176,11 +178,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 +208,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..0706ddb9b 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) {
@@ -148,7 +143,7 @@ let FeedItemInner = ({
       borderColor: pal.colors.border,
       paddingBottom:
         isThreadLastChild || (!isThreadChild && !isThreadParent)
-          ? 6
+          ? 8
           : undefined,
     },
     isThreadChild ? styles.outerSmallTop : undefined,
@@ -229,6 +224,7 @@ let FeedItemInner = ({
                     numberOfLines={1}
                     text={sanitizeDisplayName(
                       reason.by.displayName || sanitizeHandle(reason.by.handle),
+                      moderation.ui('displayName'),
                     )}
                     href={makeProfileLink(reason.by)}
                   />
@@ -246,7 +242,7 @@ let FeedItemInner = ({
             did={post.author.did}
             handle={post.author.handle}
             avatar={post.author.avatar}
-            moderation={moderation.avatar}
+            moderation={moderation.ui('avatar')}
           />
           {isThreadParent && (
             <View
@@ -264,6 +260,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 +292,7 @@ let FeedItemInner = ({
               </Text>
             </View>
           )}
+          <LabelsOnMyPost post={post} />
           <PostContent
             moderation={moderation}
             richText={richText}
@@ -306,9 +304,6 @@ let FeedItemInner = ({
             record={record}
             richText={richText}
             onPressReply={onPressReply}
-            showAppealLabelItem={
-              post.author.did === currentAccount?.did && isModeratedPost
-            }
             logContext="FeedItem"
           />
         </View>
@@ -324,7 +319,7 @@ let PostContent = ({
   postEmbed,
   postAuthor,
 }: {
-  moderation: PostModeration
+  moderation: ModerationDecision
   richText: RichTextAPI
   postEmbed: AppBskyFeedDefs.PostView['embed']
   postAuthor: AppBskyFeedDefs.PostView['author']
@@ -342,10 +337,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 +362,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..d909bda85 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,6 +43,7 @@ export function ProfileCard({
   renderButton?: (
     profile: Shadow<AppBskyActorDefs.ProfileViewBasic>,
   ) => React.ReactNode
+  onPress?: () => void
   style?: StyleProp<ViewStyle>
 }) {
   const pal = usePalette('default')
@@ -53,11 +53,8 @@ export function ProfileCard({
     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 +70,7 @@ export function ProfileCard({
       ]}
       href={makeProfileLink(profile)}
       title={profile.handle}
+      onBeforePress={onPress}
       asAnchor
       anchorNoUnderline>
       <View style={styles.layout}>
@@ -80,7 +78,7 @@ export function ProfileCard({
           <UserAvatar
             size={40}
             avatar={profile.avatar}
-            moderation={moderation.avatar}
+            moderation={moderation.ui('avatar')}
           />
         </View>
         <View style={styles.layoutContent}>
@@ -91,7 +89,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}>
@@ -119,17 +117,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 +140,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 +197,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 +219,11 @@ 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')}
+            />
           </View>
         </View>
       ))}
@@ -212,11 +236,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 +260,7 @@ export function ProfileCardWithFollowBtn({
               <FollowButton profile={profileShadow} logContext="ProfileCard" />
             )
       }
+      onPress={onPress}
     />
   )
 }
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..947d6e9cc 100644
--- a/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx
+++ b/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx
@@ -217,7 +217,7 @@ function SuggestedFollow({
         <UserAvatar
           size={60}
           avatar={profile.avatar}
-          moderation={moderation.avatar}
+          moderation={moderation.ui('avatar')}
         />
 
         <View style={{width: '100%', paddingVertical: 12}}>
@@ -227,7 +227,7 @@ function SuggestedFollow({
             numberOfLines={1}>
             {sanitizeDisplayName(
               profile.displayName || sanitizeHandle(profile.handle),
-              moderation.profile,
+              moderation.ui('displayName'),
             )}
           </Text>
           <Text
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..53dc20e71 100644
--- a/src/view/com/util/PostMeta.tsx
+++ b/src/view/com/util/PostMeta.tsx
@@ -11,7 +11,7 @@ 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 {ModerationDecision, ModerationUI} from '@atproto/api'
 import {usePrefetchProfileQuery} from '#/state/queries/profile'
 
 interface PostMetaOpts {
@@ -21,6 +21,7 @@ interface PostMetaOpts {
     handle: string
     displayName?: string | undefined
   }
+  moderation: ModerationDecision | undefined
   authorHasWarning: boolean
   postHref: string
   timestamp: string
@@ -55,9 +56,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>
     )
   }