about summary refs log tree commit diff
path: root/src/view/com/profile
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/com/profile')
-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
4 files changed, 123 insertions, 653 deletions
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.`,
               )