about summary refs log tree commit diff
path: root/src/view/screens
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/screens')
-rw-r--r--src/view/screens/DebugMod.tsx923
-rw-r--r--src/view/screens/Moderation.tsx304
-rw-r--r--src/view/screens/Profile.tsx253
-rw-r--r--src/view/screens/ProfileFeed.tsx21
-rw-r--r--src/view/screens/ProfileList.tsx18
-rw-r--r--src/view/screens/Settings/index.tsx32
-rw-r--r--src/view/screens/Storybook/Buttons.tsx41
-rw-r--r--src/view/screens/Storybook/index.tsx1
8 files changed, 1144 insertions, 449 deletions
diff --git a/src/view/screens/DebugMod.tsx b/src/view/screens/DebugMod.tsx
new file mode 100644
index 000000000..64f2376a4
--- /dev/null
+++ b/src/view/screens/DebugMod.tsx
@@ -0,0 +1,923 @@
+import React from 'react'
+import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
+import {View} from 'react-native'
+import {
+  LABELS,
+  mock,
+  moderatePost,
+  moderateProfile,
+  ModerationOpts,
+  AppBskyActorDefs,
+  AppBskyFeedDefs,
+  AppBskyFeedPost,
+  LabelPreference,
+  ModerationDecision,
+  ModerationBehavior,
+  RichText,
+  ComAtprotoLabelDefs,
+  interpretLabelValueDefinition,
+} from '@atproto/api'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {moderationOptsOverrideContext} from '#/state/queries/preferences'
+import {useSession} from '#/state/session'
+import {FeedNotification} from '#/state/queries/notifications/types'
+import {
+  groupNotifications,
+  shouldFilterNotif,
+} from '#/state/queries/notifications/util'
+
+import {atoms as a, useTheme} from '#/alf'
+import {CenteredView, ScrollView} from '#/view/com/util/Views'
+import {H1, H3, P, Text} from '#/components/Typography'
+import {useGlobalLabelStrings} from '#/lib/moderation/useGlobalLabelStrings'
+import * as Toggle from '#/components/forms/Toggle'
+import * as ToggleButton from '#/components/forms/ToggleButton'
+import {Button, ButtonIcon, ButtonText} from '#/components/Button'
+import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
+import {
+  ChevronBottom_Stroke2_Corner0_Rounded as ChevronBottom,
+  ChevronTop_Stroke2_Corner0_Rounded as ChevronTop,
+} from '#/components/icons/Chevron'
+import {ScreenHider} from '../../components/moderation/ScreenHider'
+import {ProfileHeaderStandard} from '#/screens/Profile/Header/ProfileHeaderStandard'
+import {ProfileCard} from '../com/profile/ProfileCard'
+import {FeedItem} from '../com/posts/FeedItem'
+import {FeedItem as NotifFeedItem} from '../com/notifications/FeedItem'
+import {PostThreadItem} from '../com/post-thread/PostThreadItem'
+import {Divider} from '#/components/Divider'
+
+const LABEL_VALUES: (keyof typeof LABELS)[] = Object.keys(
+  LABELS,
+) as (keyof typeof LABELS)[]
+
+export const DebugModScreen = ({}: NativeStackScreenProps<
+  CommonNavigatorParams,
+  'DebugMod'
+>) => {
+  const t = useTheme()
+  const [scenario, setScenario] = React.useState<string[]>(['label'])
+  const [scenarioSwitches, setScenarioSwitches] = React.useState<string[]>([])
+  const [label, setLabel] = React.useState<string[]>([LABEL_VALUES[0]])
+  const [target, setTarget] = React.useState<string[]>(['account'])
+  const [visibility, setVisiblity] = React.useState<string[]>(['warn'])
+  const [customLabelDef, setCustomLabelDef] =
+    React.useState<ComAtprotoLabelDefs.LabelValueDefinition>({
+      identifier: 'custom',
+      blurs: 'content',
+      severity: 'alert',
+      defaultSetting: 'warn',
+      locales: [
+        {
+          lang: 'en',
+          name: 'Custom label',
+          description: 'A custom label created in this test environment',
+        },
+      ],
+    })
+  const [view, setView] = React.useState<string[]>(['post'])
+  const labelStrings = useGlobalLabelStrings()
+  const {currentAccount} = useSession()
+
+  const isTargetMe =
+    scenario[0] === 'label' && scenarioSwitches.includes('targetMe')
+  const isSelfLabel =
+    scenario[0] === 'label' && scenarioSwitches.includes('selfLabel')
+  const noAdult =
+    scenario[0] === 'label' && scenarioSwitches.includes('noAdult')
+  const isLoggedOut =
+    scenario[0] === 'label' && scenarioSwitches.includes('loggedOut')
+  const isFollowing = scenarioSwitches.includes('following')
+
+  const did =
+    isTargetMe && currentAccount ? currentAccount.did : 'did:web:bob.test'
+
+  const profile = React.useMemo(() => {
+    const mockedProfile = mock.profileViewBasic({
+      handle: `bob.test`,
+      displayName: 'Bob Robertson',
+      description: 'User with this as their bio',
+      labels:
+        scenario[0] === 'label' && target[0] === 'account'
+          ? [
+              mock.label({
+                src: isSelfLabel ? did : undefined,
+                val: label[0],
+                uri: `at://${did}/`,
+              }),
+            ]
+          : scenario[0] === 'label' && target[0] === 'profile'
+          ? [
+              mock.label({
+                src: isSelfLabel ? did : undefined,
+                val: label[0],
+                uri: `at://${did}/app.bsky.actor.profile/self`,
+              }),
+            ]
+          : undefined,
+      viewer: mock.actorViewerState({
+        following: isFollowing
+          ? `at://${currentAccount?.did || ''}/app.bsky.graph.follow/1234`
+          : undefined,
+        muted: scenario[0] === 'mute',
+        mutedByList: undefined,
+        blockedBy: undefined,
+        blocking:
+          scenario[0] === 'block'
+            ? `at://did:web:alice.test/app.bsky.actor.block/fake`
+            : undefined,
+        blockingByList: undefined,
+      }),
+    })
+    mockedProfile.did = did
+    mockedProfile.avatar = 'https://bsky.social/about/images/favicon-32x32.png'
+    mockedProfile.banner =
+      'https://bsky.social/about/images/social-card-default-gradient.png'
+    return mockedProfile
+  }, [scenario, target, label, isSelfLabel, did, isFollowing, currentAccount])
+
+  const post = React.useMemo(() => {
+    return mock.postView({
+      record: mock.post({
+        text: "This is the body of the post. It's where the text goes. You get the idea.",
+      }),
+      author: profile,
+      labels:
+        scenario[0] === 'label' && target[0] === 'post'
+          ? [
+              mock.label({
+                src: isSelfLabel ? did : undefined,
+                val: label[0],
+                uri: `at://${did}/app.bsky.feed.post/fake`,
+              }),
+            ]
+          : undefined,
+      embed:
+        target[0] === 'embed'
+          ? mock.embedRecordView({
+              record: mock.post({
+                text: 'Embed',
+              }),
+              labels:
+                scenario[0] === 'label' && target[0] === 'embed'
+                  ? [
+                      mock.label({
+                        src: isSelfLabel ? did : undefined,
+                        val: label[0],
+                        uri: `at://${did}/app.bsky.feed.post/fake`,
+                      }),
+                    ]
+                  : undefined,
+              author: profile,
+            })
+          : {
+              $type: 'app.bsky.embed.images#view',
+              images: [
+                {
+                  thumb:
+                    'https://bsky.social/about/images/social-card-default-gradient.png',
+                  fullsize:
+                    'https://bsky.social/about/images/social-card-default-gradient.png',
+                  alt: '',
+                },
+              ],
+            },
+    })
+  }, [scenario, label, target, profile, isSelfLabel, did])
+
+  const replyNotif = React.useMemo(() => {
+    const notif = mock.replyNotification({
+      record: mock.post({
+        text: "This is the body of the post. It's where the text goes. You get the idea.",
+        reply: {
+          parent: {
+            uri: `at://${did}/app.bsky.feed.post/fake-parent`,
+            cid: 'bafyreiclp443lavogvhj3d2ob2cxbfuscni2k5jk7bebjzg7khl3esabwq',
+          },
+          root: {
+            uri: `at://${did}/app.bsky.feed.post/fake-parent`,
+            cid: 'bafyreiclp443lavogvhj3d2ob2cxbfuscni2k5jk7bebjzg7khl3esabwq',
+          },
+        },
+      }),
+      author: profile,
+      labels:
+        scenario[0] === 'label' && target[0] === 'post'
+          ? [
+              mock.label({
+                src: isSelfLabel ? did : undefined,
+                val: label[0],
+                uri: `at://${did}/app.bsky.feed.post/fake`,
+              }),
+            ]
+          : undefined,
+    })
+    const [item] = groupNotifications([notif])
+    item.subject = mock.postView({
+      record: notif.record as AppBskyFeedPost.Record,
+      author: profile,
+      labels: notif.labels,
+    })
+    return item
+  }, [scenario, label, target, profile, isSelfLabel, did])
+
+  const followNotif = React.useMemo(() => {
+    const notif = mock.followNotification({
+      author: profile,
+      subjectDid: currentAccount?.did || '',
+    })
+    const [item] = groupNotifications([notif])
+    return item
+  }, [profile, currentAccount])
+
+  const modOpts = React.useMemo(() => {
+    return {
+      userDid: isLoggedOut ? '' : isTargetMe ? did : 'did:web:alice.test',
+      prefs: {
+        adultContentEnabled: !noAdult,
+        labels: {
+          [label[0]]: visibility[0] as LabelPreference,
+        },
+        labelers: [
+          {
+            did: 'did:plc:fake-labeler',
+            labels: {[label[0]]: visibility[0] as LabelPreference},
+          },
+        ],
+        mutedWords: [],
+        hiddenPosts: [],
+      },
+      labelDefs: {
+        'did:plc:fake-labeler': [
+          interpretLabelValueDefinition(customLabelDef, 'did:plc:fake-labeler'),
+        ],
+      },
+    }
+  }, [label, visibility, noAdult, isLoggedOut, isTargetMe, did, customLabelDef])
+
+  const profileModeration = React.useMemo(() => {
+    return moderateProfile(profile, modOpts)
+  }, [profile, modOpts])
+  const postModeration = React.useMemo(() => {
+    return moderatePost(post, modOpts)
+  }, [post, modOpts])
+
+  return (
+    <moderationOptsOverrideContext.Provider value={modOpts}>
+      <ScrollView>
+        <CenteredView style={[t.atoms.bg, a.px_lg, a.py_lg]}>
+          <H1 style={[a.text_5xl, a.font_bold, a.pb_lg]}>Moderation states</H1>
+
+          <Heading title="" subtitle="Scenario" />
+          <ToggleButton.Group
+            label="Scenario"
+            values={scenario}
+            onChange={setScenario}>
+            <ToggleButton.Button name="label" label="Label">
+              Label
+            </ToggleButton.Button>
+            <ToggleButton.Button name="block" label="Block">
+              Block
+            </ToggleButton.Button>
+            <ToggleButton.Button name="mute" label="Mute">
+              Mute
+            </ToggleButton.Button>
+          </ToggleButton.Group>
+
+          {scenario[0] === 'label' && (
+            <>
+              <View
+                style={[
+                  a.border,
+                  a.rounded_sm,
+                  a.mt_lg,
+                  a.mb_lg,
+                  a.p_lg,
+                  t.atoms.border_contrast_medium,
+                ]}>
+                <Toggle.Group
+                  label="Toggle"
+                  type="radio"
+                  values={label}
+                  onChange={setLabel}>
+                  <View style={[a.flex_row, a.gap_md, a.flex_wrap]}>
+                    {LABEL_VALUES.map(labelValue => {
+                      let targetFixed = target[0]
+                      if (
+                        targetFixed !== 'account' &&
+                        targetFixed !== 'profile'
+                      ) {
+                        targetFixed = 'content'
+                      }
+                      const disabled =
+                        isSelfLabel &&
+                        LABELS[labelValue].flags.includes('no-self')
+                      return (
+                        <Toggle.Item
+                          key={labelValue}
+                          name={labelValue}
+                          label={labelStrings[labelValue].name}
+                          disabled={disabled}
+                          style={disabled ? {opacity: 0.5} : undefined}>
+                          <Toggle.Radio />
+                          <Toggle.Label>{labelValue}</Toggle.Label>
+                        </Toggle.Item>
+                      )
+                    })}
+                    <Toggle.Item
+                      name="custom"
+                      label="Custom label"
+                      disabled={isSelfLabel}
+                      style={isSelfLabel ? {opacity: 0.5} : undefined}>
+                      <Toggle.Radio />
+                      <Toggle.Label>Custom label</Toggle.Label>
+                    </Toggle.Item>
+                  </View>
+                </Toggle.Group>
+
+                {label[0] === 'custom' ? (
+                  <CustomLabelForm
+                    def={customLabelDef}
+                    setDef={setCustomLabelDef}
+                  />
+                ) : (
+                  <>
+                    <View style={{height: 10}} />
+                    <Divider />
+                  </>
+                )}
+
+                <View style={{height: 10}} />
+
+                <SmallToggler label="Advanced">
+                  <Toggle.Group
+                    label="Toggle"
+                    type="checkbox"
+                    values={scenarioSwitches}
+                    onChange={setScenarioSwitches}>
+                    <View style={[a.gap_md, a.flex_row, a.flex_wrap, a.pt_md]}>
+                      <Toggle.Item name="targetMe" label="Target is me">
+                        <Toggle.Checkbox />
+                        <Toggle.Label>Target is me</Toggle.Label>
+                      </Toggle.Item>
+                      <Toggle.Item name="following" label="Following target">
+                        <Toggle.Checkbox />
+                        <Toggle.Label>Following target</Toggle.Label>
+                      </Toggle.Item>
+                      <Toggle.Item name="selfLabel" label="Self label">
+                        <Toggle.Checkbox />
+                        <Toggle.Label>Self label</Toggle.Label>
+                      </Toggle.Item>
+                      <Toggle.Item name="noAdult" label="Adult disabled">
+                        <Toggle.Checkbox />
+                        <Toggle.Label>Adult disabled</Toggle.Label>
+                      </Toggle.Item>
+                      <Toggle.Item name="loggedOut" label="Logged out">
+                        <Toggle.Checkbox />
+                        <Toggle.Label>Logged out</Toggle.Label>
+                      </Toggle.Item>
+                    </View>
+                  </Toggle.Group>
+
+                  {LABELS[label[0] as keyof typeof LABELS]?.configurable !==
+                    false && (
+                    <View style={[a.mt_md]}>
+                      <Text
+                        style={[a.font_bold, a.text_xs, t.atoms.text, a.pb_sm]}>
+                        Preference
+                      </Text>
+                      <Toggle.Group
+                        label="Preference"
+                        type="radio"
+                        values={visibility}
+                        onChange={setVisiblity}>
+                        <View
+                          style={[
+                            a.flex_row,
+                            a.gap_md,
+                            a.flex_wrap,
+                            a.align_center,
+                          ]}>
+                          <Toggle.Item name="hide" label="Hide">
+                            <Toggle.Radio />
+                            <Toggle.Label>Hide</Toggle.Label>
+                          </Toggle.Item>
+                          <Toggle.Item name="warn" label="Warn">
+                            <Toggle.Radio />
+                            <Toggle.Label>Warn</Toggle.Label>
+                          </Toggle.Item>
+                          <Toggle.Item name="ignore" label="Ignore">
+                            <Toggle.Radio />
+                            <Toggle.Label>Ignore</Toggle.Label>
+                          </Toggle.Item>
+                        </View>
+                      </Toggle.Group>
+                    </View>
+                  )}
+                </SmallToggler>
+              </View>
+
+              <View style={[a.flex_row, a.flex_wrap, a.gap_md]}>
+                <View>
+                  <Text
+                    style={[
+                      a.font_bold,
+                      a.text_xs,
+                      t.atoms.text,
+                      a.pl_md,
+                      a.pb_xs,
+                    ]}>
+                    Target
+                  </Text>
+                  <View
+                    style={[
+                      a.border,
+                      a.rounded_full,
+                      a.px_md,
+                      a.py_sm,
+                      t.atoms.border_contrast_medium,
+                      t.atoms.bg,
+                    ]}>
+                    <Toggle.Group
+                      label="Target"
+                      type="radio"
+                      values={target}
+                      onChange={setTarget}>
+                      <View style={[a.flex_row, a.gap_md, a.flex_wrap]}>
+                        <Toggle.Item name="account" label="Account">
+                          <Toggle.Radio />
+                          <Toggle.Label>Account</Toggle.Label>
+                        </Toggle.Item>
+                        <Toggle.Item name="profile" label="Profile">
+                          <Toggle.Radio />
+                          <Toggle.Label>Profile</Toggle.Label>
+                        </Toggle.Item>
+                        <Toggle.Item name="post" label="Post">
+                          <Toggle.Radio />
+                          <Toggle.Label>Post</Toggle.Label>
+                        </Toggle.Item>
+                        <Toggle.Item name="embed" label="Embed">
+                          <Toggle.Radio />
+                          <Toggle.Label>Embed</Toggle.Label>
+                        </Toggle.Item>
+                      </View>
+                    </Toggle.Group>
+                  </View>
+                </View>
+              </View>
+            </>
+          )}
+
+          <Spacer />
+
+          <Heading title="" subtitle="Results" />
+
+          <ToggleButton.Group label="Results" values={view} onChange={setView}>
+            <ToggleButton.Button name="post" label="Post">
+              Post
+            </ToggleButton.Button>
+            <ToggleButton.Button name="notifications" label="Notifications">
+              Notifications
+            </ToggleButton.Button>
+            <ToggleButton.Button name="account" label="Account">
+              Account
+            </ToggleButton.Button>
+            <ToggleButton.Button name="data" label="Data">
+              Data
+            </ToggleButton.Button>
+          </ToggleButton.Group>
+
+          <View
+            style={[
+              a.border,
+              a.rounded_sm,
+              a.mt_lg,
+              a.p_md,
+              t.atoms.border_contrast_medium,
+            ]}>
+            {view[0] === 'post' && (
+              <>
+                <Heading title="Post" subtitle="in feed" />
+                <MockPostFeedItem post={post} moderation={postModeration} />
+
+                <Heading title="Post" subtitle="viewed directly" />
+                <MockPostThreadItem post={post} moderation={postModeration} />
+
+                <Heading title="Post" subtitle="reply in thread" />
+                <MockPostThreadItem
+                  post={post}
+                  moderation={postModeration}
+                  reply
+                />
+              </>
+            )}
+
+            {view[0] === 'notifications' && (
+              <>
+                <Heading title="Notification" subtitle="quote or reply" />
+                <MockNotifItem notif={replyNotif} moderationOpts={modOpts} />
+                <View style={{height: 20}} />
+                <Heading title="Notification" subtitle="follow or like" />
+                <MockNotifItem notif={followNotif} moderationOpts={modOpts} />
+              </>
+            )}
+
+            {view[0] === 'account' && (
+              <>
+                <Heading title="Account" subtitle="in listing" />
+                <MockAccountCard
+                  profile={profile}
+                  moderation={profileModeration}
+                />
+
+                <Heading title="Account" subtitle="viewing directly" />
+                <MockAccountScreen
+                  profile={profile}
+                  moderation={profileModeration}
+                  moderationOpts={modOpts}
+                />
+              </>
+            )}
+
+            {view[0] === 'data' && (
+              <>
+                <ModerationUIView
+                  label="Profile Moderation UI"
+                  mod={profileModeration}
+                />
+                <ModerationUIView
+                  label="Post Moderation UI"
+                  mod={postModeration}
+                />
+                <DataView
+                  label={label[0]}
+                  data={LABELS[label[0] as keyof typeof LABELS]}
+                />
+                <DataView
+                  label="Profile Moderation Data"
+                  data={profileModeration}
+                />
+                <DataView label="Post Moderation Data" data={postModeration} />
+              </>
+            )}
+          </View>
+
+          <View style={{height: 400}} />
+        </CenteredView>
+      </ScrollView>
+    </moderationOptsOverrideContext.Provider>
+  )
+}
+
+function Heading({title, subtitle}: {title: string; subtitle?: string}) {
+  const t = useTheme()
+  return (
+    <H3 style={[a.text_3xl, a.font_bold, a.pb_md]}>
+      {title}{' '}
+      {!!subtitle && (
+        <H3 style={[t.atoms.text_contrast_medium, a.text_lg]}>{subtitle}</H3>
+      )}
+    </H3>
+  )
+}
+
+function CustomLabelForm({
+  def,
+  setDef,
+}: {
+  def: ComAtprotoLabelDefs.LabelValueDefinition
+  setDef: React.Dispatch<
+    React.SetStateAction<ComAtprotoLabelDefs.LabelValueDefinition>
+  >
+}) {
+  const t = useTheme()
+  return (
+    <View
+      style={[
+        a.flex_row,
+        a.flex_wrap,
+        a.gap_md,
+        t.atoms.bg_contrast_25,
+        a.rounded_md,
+        a.p_md,
+        a.mt_md,
+      ]}>
+      <View>
+        <Text style={[a.font_bold, a.text_xs, t.atoms.text, a.pl_md, a.pb_xs]}>
+          Blurs
+        </Text>
+        <View
+          style={[
+            a.border,
+            a.rounded_full,
+            a.px_md,
+            a.py_sm,
+            t.atoms.border_contrast_medium,
+            t.atoms.bg,
+          ]}>
+          <Toggle.Group
+            label="Blurs"
+            type="radio"
+            values={[def.blurs]}
+            onChange={values => setDef(v => ({...v, blurs: values[0]}))}>
+            <View style={[a.flex_row, a.gap_md, a.flex_wrap]}>
+              <Toggle.Item name="content" label="Content">
+                <Toggle.Radio />
+                <Toggle.Label>Content</Toggle.Label>
+              </Toggle.Item>
+              <Toggle.Item name="media" label="Media">
+                <Toggle.Radio />
+                <Toggle.Label>Media</Toggle.Label>
+              </Toggle.Item>
+              <Toggle.Item name="none" label="None">
+                <Toggle.Radio />
+                <Toggle.Label>None</Toggle.Label>
+              </Toggle.Item>
+            </View>
+          </Toggle.Group>
+        </View>
+      </View>
+      <View>
+        <Text style={[a.font_bold, a.text_xs, t.atoms.text, a.pl_md, a.pb_xs]}>
+          Severity
+        </Text>
+        <View
+          style={[
+            a.border,
+            a.rounded_full,
+            a.px_md,
+            a.py_sm,
+            t.atoms.border_contrast_medium,
+            t.atoms.bg,
+          ]}>
+          <Toggle.Group
+            label="Severity"
+            type="radio"
+            values={[def.severity]}
+            onChange={values => setDef(v => ({...v, severity: values[0]}))}>
+            <View style={[a.flex_row, a.gap_md, a.flex_wrap, a.align_center]}>
+              <Toggle.Item name="alert" label="Alert">
+                <Toggle.Radio />
+                <Toggle.Label>Alert</Toggle.Label>
+              </Toggle.Item>
+              <Toggle.Item name="inform" label="Inform">
+                <Toggle.Radio />
+                <Toggle.Label>Inform</Toggle.Label>
+              </Toggle.Item>
+              <Toggle.Item name="none" label="None">
+                <Toggle.Radio />
+                <Toggle.Label>None</Toggle.Label>
+              </Toggle.Item>
+            </View>
+          </Toggle.Group>
+        </View>
+      </View>
+    </View>
+  )
+}
+
+function Toggler({label, children}: React.PropsWithChildren<{label: string}>) {
+  const t = useTheme()
+  const [show, setShow] = React.useState(false)
+  return (
+    <View style={a.mb_md}>
+      <View
+        style={[
+          t.atoms.border_contrast_medium,
+          a.border,
+          a.rounded_sm,
+          a.p_xs,
+        ]}>
+        <Button
+          variant="solid"
+          color="secondary"
+          label="Toggle visibility"
+          size="small"
+          onPress={() => setShow(!show)}>
+          <ButtonText>{label}</ButtonText>
+          <ButtonIcon
+            icon={show ? ChevronTop : ChevronBottom}
+            position="right"
+          />
+        </Button>
+        {show && children}
+      </View>
+    </View>
+  )
+}
+
+function SmallToggler({
+  label,
+  children,
+}: React.PropsWithChildren<{label: string}>) {
+  const [show, setShow] = React.useState(false)
+  return (
+    <View>
+      <View style={[a.flex_row]}>
+        <Button
+          variant="ghost"
+          color="secondary"
+          label="Toggle visibility"
+          size="tiny"
+          onPress={() => setShow(!show)}>
+          <ButtonText>{label}</ButtonText>
+          <ButtonIcon
+            icon={show ? ChevronTop : ChevronBottom}
+            position="right"
+          />
+        </Button>
+      </View>
+      {show && children}
+    </View>
+  )
+}
+
+function DataView({label, data}: {label: string; data: any}) {
+  return (
+    <Toggler label={label}>
+      <Text style={[{fontFamily: 'monospace'}, a.p_md]}>
+        {JSON.stringify(data, null, 2)}
+      </Text>
+    </Toggler>
+  )
+}
+
+function ModerationUIView({
+  mod,
+  label,
+}: {
+  mod: ModerationDecision
+  label: string
+}) {
+  return (
+    <Toggler label={label}>
+      <View style={a.p_lg}>
+        {[
+          'profileList',
+          'profileView',
+          'avatar',
+          'banner',
+          'displayName',
+          'contentList',
+          'contentView',
+          'contentMedia',
+        ].map(key => {
+          const ui = mod.ui(key as keyof ModerationBehavior)
+          return (
+            <View key={key} style={[a.flex_row, a.gap_md]}>
+              <Text style={[a.font_bold, {width: 100}]}>{key}</Text>
+              <Flag v={ui.filter} label="Filter" />
+              <Flag v={ui.blur} label="Blur" />
+              <Flag v={ui.alert} label="Alert" />
+              <Flag v={ui.inform} label="Inform" />
+              <Flag v={ui.noOverride} label="No-override" />
+            </View>
+          )
+        })}
+      </View>
+    </Toggler>
+  )
+}
+
+function Spacer() {
+  return <View style={{height: 30}} />
+}
+
+function MockPostFeedItem({
+  post,
+  moderation,
+}: {
+  post: AppBskyFeedDefs.PostView
+  moderation: ModerationDecision
+}) {
+  const t = useTheme()
+  if (moderation.ui('contentList').filter) {
+    return (
+      <P style={[t.atoms.bg_contrast_25, a.px_lg, a.py_md, a.mb_lg]}>
+        Filtered from the feed
+      </P>
+    )
+  }
+  return (
+    <FeedItem
+      post={post}
+      record={post.record as AppBskyFeedPost.Record}
+      moderation={moderation}
+      reason={undefined}
+    />
+  )
+}
+
+function MockPostThreadItem({
+  post,
+  reply,
+}: {
+  post: AppBskyFeedDefs.PostView
+  moderation: ModerationDecision
+  reply?: boolean
+}) {
+  return (
+    <PostThreadItem
+      // @ts-ignore
+      post={post}
+      record={post.record as AppBskyFeedPost.Record}
+      depth={reply ? 1 : 0}
+      isHighlightedPost={!reply}
+      treeView={false}
+      prevPost={undefined}
+      nextPost={undefined}
+      hasPrecedingItem={false}
+      onPostReply={() => {}}
+    />
+  )
+}
+
+function MockNotifItem({
+  notif,
+  moderationOpts,
+}: {
+  notif: FeedNotification
+  moderationOpts: ModerationOpts
+}) {
+  const t = useTheme()
+  if (shouldFilterNotif(notif.notification, moderationOpts)) {
+    return (
+      <P style={[t.atoms.bg_contrast_25, a.px_lg, a.py_md]}>
+        Filtered from the feed
+      </P>
+    )
+  }
+  return <NotifFeedItem item={notif} moderationOpts={moderationOpts} />
+}
+
+function MockAccountCard({
+  profile,
+  moderation,
+}: {
+  profile: AppBskyActorDefs.ProfileViewBasic
+  moderation: ModerationDecision
+}) {
+  const t = useTheme()
+
+  if (moderation.ui('profileList').filter) {
+    return (
+      <P style={[t.atoms.bg_contrast_25, a.px_lg, a.py_md, a.mb_lg]}>
+        Filtered from the listing
+      </P>
+    )
+  }
+
+  return <ProfileCard profile={profile} />
+}
+
+function MockAccountScreen({
+  profile,
+  moderation,
+  moderationOpts,
+}: {
+  profile: AppBskyActorDefs.ProfileViewBasic
+  moderation: ModerationDecision
+  moderationOpts: ModerationOpts
+}) {
+  const t = useTheme()
+  const {_} = useLingui()
+  return (
+    <View style={[t.atoms.border_contrast_medium, a.border, a.mb_md]}>
+      <ScreenHider
+        style={{}}
+        screenDescription={_(msg`profile`)}
+        modui={moderation.ui('profileView')}>
+        <ProfileHeaderStandard
+          // @ts-ignore ProfileViewBasic is close enough -prf
+          profile={profile}
+          moderationOpts={moderationOpts}
+          descriptionRT={new RichText({text: profile.description as string})}
+        />
+      </ScreenHider>
+    </View>
+  )
+}
+
+function Flag({v, label}: {v: boolean | undefined; label: string}) {
+  const t = useTheme()
+  return (
+    <View style={[a.flex_row, a.align_center, a.gap_xs]}>
+      <View
+        style={[
+          a.justify_center,
+          a.align_center,
+          a.rounded_xs,
+          a.border,
+          t.atoms.border_contrast_medium,
+          {
+            backgroundColor: t.palette.contrast_25,
+            width: 14,
+            height: 14,
+          },
+        ]}>
+        {v && <Check size="xs" fill={t.palette.contrast_900} />}
+      </View>
+      <P style={a.text_xs}>{label}</P>
+    </View>
+  )
+}
diff --git a/src/view/screens/Moderation.tsx b/src/view/screens/Moderation.tsx
deleted file mode 100644
index 928766c30..000000000
--- a/src/view/screens/Moderation.tsx
+++ /dev/null
@@ -1,304 +0,0 @@
-import React from 'react'
-import {
-  ActivityIndicator,
-  StyleSheet,
-  TouchableOpacity,
-  View,
-} from 'react-native'
-import {useFocusEffect} from '@react-navigation/native'
-import {
-  FontAwesomeIcon,
-  FontAwesomeIconStyle,
-} from '@fortawesome/react-native-fontawesome'
-import {ComAtprotoLabelDefs} from '@atproto/api'
-import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
-import {s} from 'lib/styles'
-import {CenteredView} from '../com/util/Views'
-import {ViewHeader} from '../com/util/ViewHeader'
-import {Link, TextLink} from '../com/util/Link'
-import {Text} from '../com/util/text/Text'
-import {usePalette} from 'lib/hooks/usePalette'
-import {useAnalytics} from 'lib/analytics/analytics'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {useSetMinimalShellMode} from '#/state/shell'
-import {useModalControls} from '#/state/modals'
-import {Trans, msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {ToggleButton} from '../com/util/forms/ToggleButton'
-import {useSession} from '#/state/session'
-import {
-  useProfileQuery,
-  useProfileUpdateMutation,
-} from '#/state/queries/profile'
-import {ScrollView} from '../com/util/Views'
-import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
-
-type Props = NativeStackScreenProps<CommonNavigatorParams, 'Moderation'>
-export function ModerationScreen({}: Props) {
-  const pal = usePalette('default')
-  const {_} = useLingui()
-  const setMinimalShellMode = useSetMinimalShellMode()
-  const {screen, track} = useAnalytics()
-  const {isTabletOrDesktop} = useWebMediaQueries()
-  const {openModal} = useModalControls()
-  const {mutedWordsDialogControl} = useGlobalDialogsControlContext()
-
-  useFocusEffect(
-    React.useCallback(() => {
-      screen('Moderation')
-      setMinimalShellMode(false)
-    }, [screen, setMinimalShellMode]),
-  )
-
-  const onPressContentFiltering = React.useCallback(() => {
-    track('Moderation:ContentfilteringButtonClicked')
-    openModal({name: 'content-filtering-settings'})
-  }, [track, openModal])
-
-  return (
-    <CenteredView
-      style={[
-        s.hContentRegion,
-        pal.border,
-        isTabletOrDesktop ? styles.desktopContainer : pal.viewLight,
-      ]}
-      testID="moderationScreen">
-      <ViewHeader title={_(msg`Moderation`)} showOnDesktop />
-      <ScrollView contentContainerStyle={[styles.noBorder]}>
-        <View style={styles.spacer} />
-        <TouchableOpacity
-          testID="contentFilteringBtn"
-          style={[styles.linkCard, pal.view]}
-          onPress={onPressContentFiltering}
-          accessibilityRole="tab"
-          accessibilityHint=""
-          accessibilityLabel={_(msg`Open content filtering settings`)}>
-          <View style={[styles.iconContainer, pal.btn]}>
-            <FontAwesomeIcon
-              icon="eye"
-              style={pal.text as FontAwesomeIconStyle}
-            />
-          </View>
-          <Text type="lg" style={pal.text}>
-            <Trans>Content filtering</Trans>
-          </Text>
-        </TouchableOpacity>
-        <TouchableOpacity
-          testID="mutedWordsBtn"
-          style={[styles.linkCard, pal.view]}
-          onPress={() => mutedWordsDialogControl.open()}
-          accessibilityRole="tab"
-          accessibilityHint=""
-          accessibilityLabel={_(msg`Open muted words settings`)}>
-          <View style={[styles.iconContainer, pal.btn]}>
-            <FontAwesomeIcon
-              icon="filter"
-              style={pal.text as FontAwesomeIconStyle}
-            />
-          </View>
-          <Text type="lg" style={pal.text}>
-            <Trans>Muted words & tags</Trans>
-          </Text>
-        </TouchableOpacity>
-        <Link
-          testID="moderationlistsBtn"
-          style={[styles.linkCard, pal.view]}
-          href="/moderation/modlists">
-          <View style={[styles.iconContainer, pal.btn]}>
-            <FontAwesomeIcon
-              icon="users-slash"
-              style={pal.text as FontAwesomeIconStyle}
-            />
-          </View>
-          <Text type="lg" style={pal.text}>
-            <Trans>Moderation lists</Trans>
-          </Text>
-        </Link>
-        <Link
-          testID="mutedAccountsBtn"
-          style={[styles.linkCard, pal.view]}
-          href="/moderation/muted-accounts">
-          <View style={[styles.iconContainer, pal.btn]}>
-            <FontAwesomeIcon
-              icon="user-slash"
-              style={pal.text as FontAwesomeIconStyle}
-            />
-          </View>
-          <Text type="lg" style={pal.text}>
-            <Trans>Muted accounts</Trans>
-          </Text>
-        </Link>
-        <Link
-          testID="blockedAccountsBtn"
-          style={[styles.linkCard, pal.view]}
-          href="/moderation/blocked-accounts">
-          <View style={[styles.iconContainer, pal.btn]}>
-            <FontAwesomeIcon
-              icon="ban"
-              style={pal.text as FontAwesomeIconStyle}
-            />
-          </View>
-          <Text type="lg" style={pal.text}>
-            <Trans>Blocked accounts</Trans>
-          </Text>
-        </Link>
-        <Text
-          type="xl-bold"
-          style={[
-            pal.text,
-            {
-              paddingHorizontal: 18,
-              paddingTop: 18,
-              paddingBottom: 6,
-            },
-          ]}>
-          <Trans>Logged-out visibility</Trans>
-        </Text>
-        <PwiOptOut />
-      </ScrollView>
-    </CenteredView>
-  )
-}
-
-function PwiOptOut() {
-  const pal = usePalette('default')
-  const {_} = useLingui()
-  const {currentAccount} = useSession()
-  const {data: profile} = useProfileQuery({did: currentAccount?.did})
-  const updateProfile = useProfileUpdateMutation()
-
-  const isOptedOut =
-    profile?.labels?.some(l => l.val === '!no-unauthenticated') || false
-  const canToggle = profile && !updateProfile.isPending
-
-  const onToggleOptOut = React.useCallback(() => {
-    if (!profile) {
-      return
-    }
-    let wasAdded = false
-    updateProfile.mutate({
-      profile,
-      updates: existing => {
-        // create labels attr if needed
-        existing.labels = ComAtprotoLabelDefs.isSelfLabels(existing.labels)
-          ? existing.labels
-          : {
-              $type: 'com.atproto.label.defs#selfLabels',
-              values: [],
-            }
-
-        // toggle the label
-        const hasLabel = existing.labels.values.some(
-          l => l.val === '!no-unauthenticated',
-        )
-        if (hasLabel) {
-          wasAdded = false
-          existing.labels.values = existing.labels.values.filter(
-            l => l.val !== '!no-unauthenticated',
-          )
-        } else {
-          wasAdded = true
-          existing.labels.values.push({val: '!no-unauthenticated'})
-        }
-
-        // delete if no longer needed
-        if (existing.labels.values.length === 0) {
-          delete existing.labels
-        }
-        return existing
-      },
-      checkCommitted: res => {
-        const exists = !!res.data.labels?.some(
-          l => l.val === '!no-unauthenticated',
-        )
-        return exists === wasAdded
-      },
-    })
-  }, [updateProfile, profile])
-
-  return (
-    <View style={[pal.view, styles.toggleCard]}>
-      <View
-        style={{flexDirection: 'row', alignItems: 'center', paddingRight: 14}}>
-        <ToggleButton
-          type="default-light"
-          label={_(
-            msg`Discourage apps from showing my account to logged-out users`,
-          )}
-          labelType="lg"
-          isSelected={isOptedOut}
-          onPress={canToggle ? onToggleOptOut : undefined}
-          style={[canToggle ? undefined : {opacity: 0.5}, {flex: 1}]}
-        />
-        {updateProfile.isPending && <ActivityIndicator />}
-      </View>
-      <View
-        style={{
-          flexDirection: 'column',
-          gap: 10,
-          paddingLeft: 66,
-          paddingRight: 12,
-          paddingBottom: 10,
-          marginBottom: 64,
-        }}>
-        <Text style={pal.textLight}>
-          <Trans>
-            Bluesky will not show your profile and posts to logged-out users.
-            Other apps may not honor this request. This does not make your
-            account private.
-          </Trans>
-        </Text>
-        <Text style={[pal.textLight, {fontWeight: '500'}]}>
-          <Trans>
-            Note: Bluesky is an open and public network. This setting only
-            limits the visibility of your content on the Bluesky app and
-            website, and other apps may not respect this setting. Your content
-            may still be shown to logged-out users by other apps and websites.
-          </Trans>
-        </Text>
-        <TextLink
-          style={pal.link}
-          href="https://blueskyweb.zendesk.com/hc/en-us/articles/15835264007693-Data-Privacy"
-          text={_(msg`Learn more about what is public on Bluesky.`)}
-        />
-      </View>
-    </View>
-  )
-}
-
-const styles = StyleSheet.create({
-  desktopContainer: {
-    borderLeftWidth: 1,
-    borderRightWidth: 1,
-  },
-  spacer: {
-    height: 6,
-  },
-  linkCard: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    paddingVertical: 12,
-    paddingHorizontal: 18,
-    marginBottom: 1,
-  },
-  toggleCard: {
-    paddingVertical: 8,
-    paddingTop: 2,
-    paddingHorizontal: 6,
-    marginBottom: 1,
-  },
-  iconContainer: {
-    alignItems: 'center',
-    justifyContent: 'center',
-    width: 40,
-    height: 40,
-    borderRadius: 30,
-    marginRight: 12,
-  },
-  noBorder: {
-    borderBottomWidth: 0,
-    borderRightWidth: 0,
-    borderLeftWidth: 0,
-    borderTopWidth: 0,
-  },
-})
diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx
index b30b4491b..d5a46c5c9 100644
--- a/src/view/screens/Profile.tsx
+++ b/src/view/screens/Profile.tsx
@@ -1,5 +1,5 @@
 import React, {useMemo} from 'react'
-import {StyleSheet, View} from 'react-native'
+import {StyleSheet} from 'react-native'
 import {useFocusEffect} from '@react-navigation/native'
 import {
   AppBskyActorDefs,
@@ -7,48 +7,39 @@ import {
   ModerationOpts,
   RichText as RichTextAPI,
 } from '@atproto/api'
-import {msg, Trans} from '@lingui/macro'
+import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
 import {CenteredView} from '../com/util/Views'
 import {ListRef} from '../com/util/List'
-import {ScreenHider} from 'view/com/util/moderation/ScreenHider'
-import {Feed} from 'view/com/posts/Feed'
+import {ScreenHider} from '#/components/moderation/ScreenHider'
 import {ProfileLists} from '../com/lists/ProfileLists'
 import {ProfileFeedgens} from '../com/feeds/ProfileFeedgens'
-import {ProfileHeader, ProfileHeaderLoading} from '../com/profile/ProfileHeader'
 import {PagerWithHeader} from 'view/com/pager/PagerWithHeader'
 import {ErrorScreen} from '../com/util/error/ErrorScreen'
-import {EmptyState} from '../com/util/EmptyState'
 import {FAB} from '../com/util/fab/FAB'
 import {s, colors} from 'lib/styles'
 import {useAnalytics} from 'lib/analytics/analytics'
 import {ComposeIcon2} from 'lib/icons'
 import {useSetTitle} from 'lib/hooks/useSetTitle'
 import {combinedDisplayName} from 'lib/strings/display-names'
-import {
-  FeedDescriptor,
-  resetProfilePostsQueries,
-} from '#/state/queries/post-feed'
+import {resetProfilePostsQueries} from '#/state/queries/post-feed'
 import {useResolveDidQuery} from '#/state/queries/resolve-uri'
 import {useProfileQuery} from '#/state/queries/profile'
 import {useProfileShadow} from '#/state/cache/profile-shadow'
 import {useSession, getAgent} from '#/state/session'
 import {useModerationOpts} from '#/state/queries/preferences'
-import {useProfileExtraInfoQuery} from '#/state/queries/profile-extra-info'
-import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
+import {useLabelerInfoQuery} from '#/state/queries/labeler'
 import {useSetDrawerSwipeDisabled, useSetMinimalShellMode} from '#/state/shell'
 import {cleanError} from '#/lib/strings/errors'
-import {LoadLatestBtn} from '../com/util/load-latest/LoadLatestBtn'
-import {useQueryClient} from '@tanstack/react-query'
 import {useComposerControls} from '#/state/shell/composer'
 import {listenSoftReset} from '#/state/events'
-import {truncateAndInvalidate} from '#/state/queries/util'
-import {Text} from '#/view/com/util/text/Text'
-import {usePalette} from 'lib/hooks/usePalette'
-import {isNative} from '#/platform/detection'
 import {isInvalidHandle} from '#/lib/strings/handles'
 
+import {ProfileFeedSection} from '#/screens/Profile/Sections/Feed'
+import {ProfileLabelsSection} from '#/screens/Profile/Sections/Labels'
+import {ProfileHeader, ProfileHeaderLoading} from '#/screens/Profile/Header'
+
 interface SectionRef {
   scrollToTop: () => void
 }
@@ -148,16 +139,24 @@ function ProfileScreenLoaded({
   const setMinimalShellMode = useSetMinimalShellMode()
   const {openComposer} = useComposerControls()
   const {screen, track} = useAnalytics()
+  const {
+    data: labelerInfo,
+    error: labelerError,
+    isLoading: isLabelerLoading,
+  } = useLabelerInfoQuery({
+    did: profile.did,
+    enabled: !!profile.associated?.labeler,
+  })
   const [currentPage, setCurrentPage] = React.useState(0)
   const {_} = useLingui()
   const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
-  const extraInfoQuery = useProfileExtraInfoQuery(profile.did)
   const postsSectionRef = React.useRef<SectionRef>(null)
   const repliesSectionRef = React.useRef<SectionRef>(null)
   const mediaSectionRef = React.useRef<SectionRef>(null)
   const likesSectionRef = React.useRef<SectionRef>(null)
   const feedsSectionRef = React.useRef<SectionRef>(null)
   const listsSectionRef = React.useRef<SectionRef>(null)
+  const labelsSectionRef = React.useRef<SectionRef>(null)
 
   useSetTitle(combinedDisplayName(profile))
 
@@ -171,44 +170,75 @@ function ProfileScreenLoaded({
   )
 
   const isMe = profile.did === currentAccount?.did
+  const hasLabeler = !!profile.associated?.labeler
+  const showFiltersTab = hasLabeler
+  const showPostsTab = true
   const showRepliesTab = hasSession
+  const showMediaTab = !hasLabeler
   const showLikesTab = isMe
-  const showFeedsTab = hasSession && (isMe || extraInfoQuery.data?.hasFeedgens)
-  const showListsTab = hasSession && (isMe || extraInfoQuery.data?.hasLists)
+  const showFeedsTab =
+    hasSession && (isMe || (profile.associated?.feedgens || 0) > 0)
+  const showListsTab =
+    hasSession && (isMe || (profile.associated?.lists || 0) > 0)
+
   const sectionTitles = useMemo<string[]>(() => {
     return [
-      _(msg`Posts`),
+      showFiltersTab ? _(msg`Labels`) : undefined,
+      showListsTab && hasLabeler ? _(msg`Lists`) : undefined,
+      showPostsTab ? _(msg`Posts`) : undefined,
       showRepliesTab ? _(msg`Replies`) : undefined,
-      _(msg`Media`),
+      showMediaTab ? _(msg`Media`) : undefined,
       showLikesTab ? _(msg`Likes`) : undefined,
       showFeedsTab ? _(msg`Feeds`) : undefined,
-      showListsTab ? _(msg`Lists`) : undefined,
+      showListsTab && !hasLabeler ? _(msg`Lists`) : undefined,
     ].filter(Boolean) as string[]
-  }, [showRepliesTab, showLikesTab, showFeedsTab, showListsTab, _])
+  }, [
+    showPostsTab,
+    showRepliesTab,
+    showMediaTab,
+    showLikesTab,
+    showFeedsTab,
+    showListsTab,
+    showFiltersTab,
+    hasLabeler,
+    _,
+  ])
 
   let nextIndex = 0
-  const postsIndex = nextIndex++
+  let filtersIndex: number | null = null
+  let postsIndex: number | null = null
   let repliesIndex: number | null = null
+  let mediaIndex: number | null = null
+  let likesIndex: number | null = null
+  let feedsIndex: number | null = null
+  let listsIndex: number | null = null
+  if (showFiltersTab) {
+    filtersIndex = nextIndex++
+  }
+  if (showPostsTab) {
+    postsIndex = nextIndex++
+  }
   if (showRepliesTab) {
     repliesIndex = nextIndex++
   }
-  const mediaIndex = nextIndex++
-  let likesIndex: number | null = null
+  if (showMediaTab) {
+    mediaIndex = nextIndex++
+  }
   if (showLikesTab) {
     likesIndex = nextIndex++
   }
-  let feedsIndex: number | null = null
   if (showFeedsTab) {
     feedsIndex = nextIndex++
   }
-  let listsIndex: number | null = null
   if (showListsTab) {
     listsIndex = nextIndex++
   }
 
   const scrollSectionToTop = React.useCallback(
     (index: number) => {
-      if (index === postsIndex) {
+      if (index === filtersIndex) {
+        labelsSectionRef.current?.scrollToTop()
+      } else if (index === postsIndex) {
         postsSectionRef.current?.scrollToTop()
       } else if (index === repliesIndex) {
         repliesSectionRef.current?.scrollToTop()
@@ -222,7 +252,15 @@ function ProfileScreenLoaded({
         listsSectionRef.current?.scrollToTop()
       }
     },
-    [postsIndex, repliesIndex, mediaIndex, likesIndex, feedsIndex, listsIndex],
+    [
+      filtersIndex,
+      postsIndex,
+      repliesIndex,
+      mediaIndex,
+      likesIndex,
+      feedsIndex,
+      listsIndex,
+    ],
   )
 
   useFocusEffect(
@@ -278,6 +316,7 @@ function ProfileScreenLoaded({
     return (
       <ProfileHeader
         profile={profile}
+        labeler={labelerInfo}
         descriptionRT={hasDescription ? descriptionRT : null}
         moderationOpts={moderationOpts}
         hideBackButton={hideBackButton}
@@ -286,6 +325,7 @@ function ProfileScreenLoaded({
     )
   }, [
     profile,
+    labelerInfo,
     descriptionRT,
     hasDescription,
     moderationOpts,
@@ -297,8 +337,8 @@ function ProfileScreenLoaded({
     <ScreenHider
       testID="profileView"
       style={styles.container}
-      screenDescription="profile"
-      moderation={moderation.account}>
+      screenDescription={_(msg`profile`)}
+      modui={moderation.ui('profileView')}>
       <PagerWithHeader
         testID="profilePager"
         isHeaderReady={!showPlaceholder}
@@ -306,19 +346,45 @@ function ProfileScreenLoaded({
         onPageSelected={onPageSelected}
         onCurrentPageSelected={onCurrentPageSelected}
         renderHeader={renderHeader}>
-        {({headerHeight, isFocused, scrollElRef}) => (
-          <FeedSection
-            ref={postsSectionRef}
-            feed={`author|${profile.did}|posts_and_author_threads`}
-            headerHeight={headerHeight}
-            isFocused={isFocused}
-            scrollElRef={scrollElRef as ListRef}
-            ignoreFilterFor={profile.did}
-          />
-        )}
+        {showFiltersTab
+          ? ({headerHeight, scrollElRef}) => (
+              <ProfileLabelsSection
+                ref={labelsSectionRef}
+                labelerInfo={labelerInfo}
+                labelerError={labelerError}
+                isLabelerLoading={isLabelerLoading}
+                moderationOpts={moderationOpts}
+                scrollElRef={scrollElRef as ListRef}
+                headerHeight={headerHeight}
+              />
+            )
+          : null}
+        {showListsTab && !!profile.associated?.labeler
+          ? ({headerHeight, isFocused, scrollElRef}) => (
+              <ProfileLists
+                ref={listsSectionRef}
+                did={profile.did}
+                scrollElRef={scrollElRef as ListRef}
+                headerOffset={headerHeight}
+                enabled={isFocused}
+              />
+            )
+          : null}
+        {showPostsTab
+          ? ({headerHeight, isFocused, scrollElRef}) => (
+              <ProfileFeedSection
+                ref={postsSectionRef}
+                feed={`author|${profile.did}|posts_and_author_threads`}
+                headerHeight={headerHeight}
+                isFocused={isFocused}
+                scrollElRef={scrollElRef as ListRef}
+                ignoreFilterFor={profile.did}
+              />
+            )
+          : null}
         {showRepliesTab
           ? ({headerHeight, isFocused, scrollElRef}) => (
-              <FeedSection
+              <ProfileFeedSection
                 ref={repliesSectionRef}
                 feed={`author|${profile.did}|posts_with_replies`}
                 headerHeight={headerHeight}
@@ -328,19 +394,21 @@ function ProfileScreenLoaded({
               />
             )
           : null}
-        {({headerHeight, isFocused, scrollElRef}) => (
-          <FeedSection
-            ref={mediaSectionRef}
-            feed={`author|${profile.did}|posts_with_media`}
-            headerHeight={headerHeight}
-            isFocused={isFocused}
-            scrollElRef={scrollElRef as ListRef}
-            ignoreFilterFor={profile.did}
-          />
-        )}
+        {showMediaTab
+          ? ({headerHeight, isFocused, scrollElRef}) => (
+              <ProfileFeedSection
+                ref={mediaSectionRef}
+                feed={`author|${profile.did}|posts_with_media`}
+                headerHeight={headerHeight}
+                isFocused={isFocused}
+                scrollElRef={scrollElRef as ListRef}
+                ignoreFilterFor={profile.did}
+              />
+            )
+          : null}
         {showLikesTab
           ? ({headerHeight, isFocused, scrollElRef}) => (
-              <FeedSection
+              <ProfileFeedSection
                 ref={likesSectionRef}
                 feed={`likes|${profile.did}`}
                 headerHeight={headerHeight}
@@ -361,7 +429,7 @@ function ProfileScreenLoaded({
               />
             )
           : null}
-        {showListsTab
+        {showListsTab && !profile.associated?.labeler
           ? ({headerHeight, isFocused, scrollElRef}) => (
               <ProfileLists
                 ref={listsSectionRef}
@@ -387,77 +455,6 @@ function ProfileScreenLoaded({
   )
 }
 
-interface FeedSectionProps {
-  feed: FeedDescriptor
-  headerHeight: number
-  isFocused: boolean
-  scrollElRef: ListRef
-  ignoreFilterFor?: string
-}
-const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
-  function FeedSectionImpl(
-    {feed, headerHeight, isFocused, scrollElRef, ignoreFilterFor},
-    ref,
-  ) {
-    const {_} = useLingui()
-    const queryClient = useQueryClient()
-    const [hasNew, setHasNew] = React.useState(false)
-    const [isScrolledDown, setIsScrolledDown] = React.useState(false)
-
-    const onScrollToTop = React.useCallback(() => {
-      scrollElRef.current?.scrollToOffset({
-        animated: isNative,
-        offset: -headerHeight,
-      })
-      truncateAndInvalidate(queryClient, FEED_RQKEY(feed))
-      setHasNew(false)
-    }, [scrollElRef, headerHeight, queryClient, feed, setHasNew])
-    React.useImperativeHandle(ref, () => ({
-      scrollToTop: onScrollToTop,
-    }))
-
-    const renderPostsEmpty = React.useCallback(() => {
-      return <EmptyState icon="feed" message={_(msg`This feed is empty!`)} />
-    }, [_])
-
-    return (
-      <View>
-        <Feed
-          testID="postsFeed"
-          enabled={isFocused}
-          feed={feed}
-          scrollElRef={scrollElRef}
-          onHasNew={setHasNew}
-          onScrolledDownChange={setIsScrolledDown}
-          renderEmptyState={renderPostsEmpty}
-          headerOffset={headerHeight}
-          renderEndOfFeed={ProfileEndOfFeed}
-          ignoreFilterFor={ignoreFilterFor}
-        />
-        {(isScrolledDown || hasNew) && (
-          <LoadLatestBtn
-            onPress={onScrollToTop}
-            label={_(msg`Load new posts`)}
-            showIndicator={hasNew}
-          />
-        )}
-      </View>
-    )
-  },
-)
-
-function ProfileEndOfFeed() {
-  const pal = usePalette('default')
-
-  return (
-    <View style={[pal.border, {paddingTop: 32, borderTopWidth: 1}]}>
-      <Text style={[pal.textLight, pal.border, {textAlign: 'center'}]}>
-        <Trans>End of feed</Trans>
-      </Text>
-    </View>
-  )
-}
-
 function useRichText(text: string): [RichTextAPI, boolean] {
   const [prevText, setPrevText] = React.useState(text)
   const [rawRT, setRawRT] = React.useState(() => new RichTextAPI({text}))
diff --git a/src/view/screens/ProfileFeed.tsx b/src/view/screens/ProfileFeed.tsx
index b3a7328c1..416bbc30e 100644
--- a/src/view/screens/ProfileFeed.tsx
+++ b/src/view/screens/ProfileFeed.tsx
@@ -35,7 +35,7 @@ import {ComposeIcon2} from 'lib/icons'
 import {logger} from '#/logger'
 import {Trans, msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
-import {useModalControls} from '#/state/modals'
+import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog'
 import {useFeedSourceInfoQuery, FeedSourceFeedInfo} from '#/state/queries/feed'
 import {useResolveUriQuery} from '#/state/queries/resolve-uri'
 import {
@@ -155,7 +155,7 @@ export function ProfileFeedScreenInner({
   const {_} = useLingui()
   const t = useTheme()
   const {hasSession, currentAccount} = useSession()
-  const {openModal} = useModalControls()
+  const reportDialogControl = useReportDialogControl()
   const {openComposer} = useComposerControls()
   const {track} = useAnalytics()
   const feedSectionRef = React.useRef<SectionRef>(null)
@@ -253,13 +253,8 @@ export function ProfileFeedScreenInner({
   }, [feedInfo, track])
 
   const onPressReport = React.useCallback(() => {
-    if (!feedInfo) return
-    openModal({
-      name: 'report',
-      uri: feedInfo.uri,
-      cid: feedInfo.cid,
-    })
-  }, [openModal, feedInfo])
+    reportDialogControl.open()
+  }, [reportDialogControl])
 
   const onCurrentPageSelected = React.useCallback(
     (index: number) => {
@@ -400,6 +395,14 @@ export function ProfileFeedScreenInner({
 
   return (
     <View style={s.hContentRegion}>
+      <ReportDialog
+        control={reportDialogControl}
+        params={{
+          type: 'feedgen',
+          uri: feedInfo.uri,
+          cid: feedInfo.cid,
+        }}
+      />
       <PagerWithHeader
         items={SECTION_TITLES}
         isHeaderReady={true}
diff --git a/src/view/screens/ProfileList.tsx b/src/view/screens/ProfileList.tsx
index 95046e5b4..351521265 100644
--- a/src/view/screens/ProfileList.tsx
+++ b/src/view/screens/ProfileList.tsx
@@ -39,6 +39,7 @@ import {Trans, msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useSetMinimalShellMode} from '#/state/shell'
 import {useModalControls} from '#/state/modals'
+import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog'
 import {useResolveUriQuery} from '#/state/queries/resolve-uri'
 import {
   useListQuery,
@@ -236,6 +237,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
   const {_} = useLingui()
   const navigation = useNavigation<NavigationProp>()
   const {currentAccount} = useSession()
+  const reportDialogControl = useReportDialogControl()
   const {openModal} = useModalControls()
   const listMuteMutation = useListMuteMutation()
   const listBlockMutation = useListBlockMutation()
@@ -370,12 +372,8 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
   ])
 
   const onPressReport = useCallback(() => {
-    openModal({
-      name: 'report',
-      uri: list.uri,
-      cid: list.cid,
-    })
-  }, [openModal, list])
+    reportDialogControl.open()
+  }, [reportDialogControl])
 
   const onPressShare = useCallback(() => {
     const url = toShareUrl(`/profile/${list.creator.did}/lists/${rkey}`)
@@ -550,6 +548,14 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
       isOwner={list.creator.did === currentAccount?.did}
       creator={list.creator}
       avatarType="list">
+      <ReportDialog
+        control={reportDialogControl}
+        params={{
+          type: 'list',
+          uri: list.uri,
+          cid: list.cid,
+        }}
+      />
       {isCurateList || isPinned ? (
         <Button
           testID={isPinned ? 'unpinBtn' : 'pinBtn'}
diff --git a/src/view/screens/Settings/index.tsx b/src/view/screens/Settings/index.tsx
index 00b507a99..1b96a09af 100644
--- a/src/view/screens/Settings/index.tsx
+++ b/src/view/screens/Settings/index.tsx
@@ -40,7 +40,10 @@ import {
 } from '#/state/preferences'
 import {useSession, useSessionApi, SessionAccount} from '#/state/session'
 import {useProfileQuery} from '#/state/queries/profile'
-import {useClearPreferencesMutation} from '#/state/queries/preferences'
+import {
+  useClearPreferencesMutation,
+  usePreferencesQuery,
+} from '#/state/queries/preferences'
 // TODO import {useInviteCodesQuery} from '#/state/queries/invites'
 import {clear as clearStorage} from '#/state/persisted/store'
 import {clearLegacyStorage} from '#/state/persisted/legacy'
@@ -68,6 +71,7 @@ import {SelectableBtn} from 'view/com/util/forms/SelectableBtn'
 import {AccountDropdownBtn} from 'view/com/util/AccountDropdownBtn'
 import {SimpleViewHeader} from 'view/com/util/SimpleViewHeader'
 import {ExportCarDialog} from './ExportCarDialog'
+import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings'
 
 function SettingsAccountCard({account}: {account: SessionAccount}) {
   const pal = usePalette('default')
@@ -152,6 +156,7 @@ export function SettingsScreen({}: Props) {
   const {screen, track} = useAnalytics()
   const {openModal} = useModalControls()
   const {isSwitchingAccounts, accounts, currentAccount} = useSession()
+  const {data: preferences} = usePreferencesQuery()
   const {mutate: clearPreferences} = useClearPreferencesMutation()
   // TODO
   // const {data: invites} = useInviteCodesQuery()
@@ -159,6 +164,7 @@ export function SettingsScreen({}: Props) {
   const {setShowLoggedOut} = useLoggedOutViewControls()
   const closeAllActiveElements = useCloseAllActiveElements()
   const exportCarControl = useDialogControl()
+  const birthdayControl = useDialogControl()
 
   // const primaryBg = useCustomPalette<ViewStyle>({
   //   light: {backgroundColor: colors.blue0},
@@ -261,6 +267,10 @@ export function SettingsScreen({}: Props) {
     navigation.navigate('Debug')
   }, [navigation])
 
+  const onPressDebugModeration = React.useCallback(() => {
+    navigation.navigate('DebugMod')
+  }, [navigation])
+
   const onPressSavedFeeds = React.useCallback(() => {
     navigation.navigate('SavedFeeds')
   }, [navigation])
@@ -269,6 +279,10 @@ export function SettingsScreen({}: Props) {
     Linking.openURL(STATUS_PAGE_URL)
   }, [])
 
+  const onPressBirthday = React.useCallback(() => {
+    birthdayControl.open()
+  }, [birthdayControl])
+
   const clearAllStorage = React.useCallback(async () => {
     await clearStorage()
     Toast.show(_(msg`Storage cleared, you need to restart the app now.`))
@@ -281,6 +295,10 @@ export function SettingsScreen({}: Props) {
   return (
     <View style={s.hContentRegion} testID="settingsScreen">
       <ExportCarDialog control={exportCarControl} />
+      <BirthDateSettingsDialog
+        control={birthdayControl}
+        preferences={preferences}
+      />
 
       <SimpleViewHeader
         showBackButton={isMobile}
@@ -339,7 +357,7 @@ export function SettingsScreen({}: Props) {
               <Text type="lg-medium" style={pal.text}>
                 <Trans>Birthday:</Trans>{' '}
               </Text>
-              <Link onPress={() => openModal({name: 'birth-date-settings'})}>
+              <Link onPress={onPressBirthday}>
                 <Text type="lg" style={pal.link}>
                   <Trans>Show</Trans>
                 </Text>
@@ -809,6 +827,16 @@ export function SettingsScreen({}: Props) {
             </TouchableOpacity>
             <TouchableOpacity
               style={[pal.view, styles.linkCardNoIcon]}
+              onPress={onPressDebugModeration}
+              accessibilityRole="button"
+              accessibilityLabel={_(msg`Open storybook page`)}
+              accessibilityHint={_(msg`Opens the storybook page`)}>
+              <Text type="lg" style={pal.text}>
+                <Trans>Debug Moderation</Trans>
+              </Text>
+            </TouchableOpacity>
+            <TouchableOpacity
+              style={[pal.view, styles.linkCardNoIcon]}
               onPress={onPressResetPreferences}
               accessibilityRole="button"
               accessibilityLabel={_(msg`Reset preferences`)}
diff --git a/src/view/screens/Storybook/Buttons.tsx b/src/view/screens/Storybook/Buttons.tsx
index 320db13ff..ad2fff3f4 100644
--- a/src/view/screens/Storybook/Buttons.tsx
+++ b/src/view/screens/Storybook/Buttons.tsx
@@ -129,6 +129,15 @@ export function Buttons() {
           <ButtonIcon icon={Globe} position="left" />
           <ButtonText>Link out</ButtonText>
         </Button>
+
+        <Button
+          variant="gradient"
+          color="gradient_sky"
+          size="tiny"
+          label="Link out">
+          <ButtonIcon icon={Globe} position="left" />
+          <ButtonText>Link out</ButtonText>
+        </Button>
       </View>
 
       <View style={[a.flex_row, a.gap_md, a.align_start]}>
@@ -149,6 +158,14 @@ export function Buttons() {
           <ButtonIcon icon={ChevronLeft} />
         </Button>
         <Button
+          variant="gradient"
+          color="gradient_sunset"
+          size="tiny"
+          shape="round"
+          label="Link out">
+          <ButtonIcon icon={ChevronLeft} />
+        </Button>
+        <Button
           variant="outline"
           color="primary"
           size="large"
@@ -164,6 +181,14 @@ export function Buttons() {
           label="Link out">
           <ButtonIcon icon={ChevronLeft} />
         </Button>
+        <Button
+          variant="ghost"
+          color="primary"
+          size="tiny"
+          shape="round"
+          label="Link out">
+          <ButtonIcon icon={ChevronLeft} />
+        </Button>
       </View>
 
       <View style={[a.flex_row, a.gap_md, a.align_start]}>
@@ -184,6 +209,14 @@ export function Buttons() {
           <ButtonIcon icon={ChevronLeft} />
         </Button>
         <Button
+          variant="gradient"
+          color="gradient_sunset"
+          size="tiny"
+          shape="square"
+          label="Link out">
+          <ButtonIcon icon={ChevronLeft} />
+        </Button>
+        <Button
           variant="outline"
           color="primary"
           size="large"
@@ -199,6 +232,14 @@ export function Buttons() {
           label="Link out">
           <ButtonIcon icon={ChevronLeft} />
         </Button>
+        <Button
+          variant="ghost"
+          color="primary"
+          size="tiny"
+          shape="square"
+          label="Link out">
+          <ButtonIcon icon={ChevronLeft} />
+        </Button>
       </View>
     </View>
   )
diff --git a/src/view/screens/Storybook/index.tsx b/src/view/screens/Storybook/index.tsx
index e43d756de..3a2e2f369 100644
--- a/src/view/screens/Storybook/index.tsx
+++ b/src/view/screens/Storybook/index.tsx
@@ -67,6 +67,7 @@ export function Storybook() {
             </Button>
           </View>
 
+          <Dialogs />
           <ThemeProvider theme="light">
             <Theming />
           </ThemeProvider>