about summary refs log tree commit diff
diff options
context:
space:
mode:
authorEric Bailey <git@esb.lol>2025-04-15 08:13:20 -0500
committerGitHub <noreply@github.com>2025-04-15 06:13:20 -0700
commitf46336e34142e7f46bb1395f727e303e37b15d41 (patch)
treedac04a4abd8d100dc9c0d5c65ab7f75f1d3280eb
parent32c2b69b848050975b698067383dd24f2754cab2 (diff)
downloadvoidsky-f46336e34142e7f46bb1395f727e303e37b15d41.tar.zst
Replace old ProfileCard with new (#8195)
* Replace usages of old ProfileCard

* Replace Pills with Labels component

* Replace impl of ProfileCardWithFollowButton

* Remove never-used LikesDialog

* Handle missing mod opts

* Add missing profile hover

* use modern button in listmembers

* remove follow button from muted accounts list

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>
-rw-r--r--src/components/LikesDialog.tsx129
-rw-r--r--src/components/ProfileCard.tsx53
-rw-r--r--src/screens/Search/SearchResults.tsx4
-rw-r--r--src/view/com/lists/ListMembers.tsx85
-rw-r--r--src/view/com/profile/ProfileCard.tsx311
-rw-r--r--src/view/screens/DebugMod.tsx32
-rw-r--r--src/view/screens/ModerationBlockedAccounts.tsx35
-rw-r--r--src/view/screens/ModerationMutedAccounts.tsx49
8 files changed, 189 insertions, 509 deletions
diff --git a/src/components/LikesDialog.tsx b/src/components/LikesDialog.tsx
deleted file mode 100644
index cb000b433..000000000
--- a/src/components/LikesDialog.tsx
+++ /dev/null
@@ -1,129 +0,0 @@
-import {useCallback, useMemo} from 'react'
-import {ActivityIndicator, FlatList, View} from 'react-native'
-import {AppBskyFeedGetLikes as GetLikes} from '@atproto/api'
-import {msg, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-
-import {cleanError} from '#/lib/strings/errors'
-import {logger} from '#/logger'
-import {useLikedByQuery} from '#/state/queries/post-liked-by'
-import {useResolveUriQuery} from '#/state/queries/resolve-uri'
-import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
-import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
-import {atoms as a, useTheme} from '#/alf'
-import * as Dialog from '#/components/Dialog'
-import {Loader} from '#/components/Loader'
-import {Text} from '#/components/Typography'
-
-interface LikesDialogProps {
-  control: Dialog.DialogOuterProps['control']
-  uri: string
-}
-
-export function LikesDialog(props: LikesDialogProps) {
-  return (
-    <Dialog.Outer control={props.control}>
-      <Dialog.Handle />
-      <LikesDialogInner {...props} />
-    </Dialog.Outer>
-  )
-}
-
-export function LikesDialogInner({control, uri}: LikesDialogProps) {
-  const {_} = useLingui()
-  const t = useTheme()
-
-  const {
-    data: resolvedUri,
-    error: resolveError,
-    isFetched: hasFetchedResolvedUri,
-  } = useResolveUriQuery(uri)
-  const {
-    data,
-    isFetching: isFetchingLikedBy,
-    isFetched: hasFetchedLikedBy,
-    isFetchingNextPage,
-    hasNextPage,
-    fetchNextPage,
-    isError,
-    error: likedByError,
-  } = useLikedByQuery(resolvedUri?.uri)
-
-  const isLoading = !hasFetchedResolvedUri || !hasFetchedLikedBy
-  const likes = useMemo(() => {
-    if (data?.pages) {
-      return data.pages.flatMap(page => page.likes)
-    }
-    return []
-  }, [data])
-
-  const onEndReached = useCallback(async () => {
-    if (isFetchingLikedBy || !hasNextPage || isError) return
-    try {
-      await fetchNextPage()
-    } catch (err) {
-      logger.error('Failed to load more likes', {message: err})
-    }
-  }, [isFetchingLikedBy, hasNextPage, isError, fetchNextPage])
-
-  const renderItem = useCallback(
-    ({item}: {item: GetLikes.Like}) => {
-      return (
-        <ProfileCardWithFollowBtn
-          key={item.actor.did}
-          profile={item.actor}
-          onPress={() => control.close()}
-        />
-      )
-    },
-    [control],
-  )
-
-  return (
-    <Dialog.Inner label={_(msg`Users that have liked this content or profile`)}>
-      <Text style={[a.text_2xl, a.font_bold, a.leading_tight, a.pb_lg]}>
-        <Trans>Liked by</Trans>
-      </Text>
-
-      {isLoading ? (
-        <View style={{minHeight: 300}}>
-          <Loader size="xl" />
-        </View>
-      ) : resolveError || likedByError || !data ? (
-        <ErrorMessage message={cleanError(resolveError || likedByError)} />
-      ) : likes.length === 0 ? (
-        <View style={[t.atoms.bg_contrast_50, a.px_md, a.py_xl, a.rounded_md]}>
-          <Text style={[a.text_center]}>
-            <Trans>
-              Nobody has liked this yet. Maybe you should be the first!
-            </Trans>
-          </Text>
-        </View>
-      ) : (
-        <FlatList
-          data={likes}
-          keyExtractor={item => item.actor.did}
-          onEndReached={onEndReached}
-          renderItem={renderItem}
-          initialNumToRender={15}
-          ListFooterComponent={
-            <ListFooterComponent isFetching={isFetchingNextPage} />
-          }
-        />
-      )}
-
-      <Dialog.Close />
-    </Dialog.Inner>
-  )
-}
-
-function ListFooterComponent({isFetching}: {isFetching: boolean}) {
-  if (isFetching) {
-    return (
-      <View style={a.pt_lg}>
-        <ActivityIndicator />
-      </View>
-    )
-  }
-  return null
-}
diff --git a/src/components/ProfileCard.tsx b/src/components/ProfileCard.tsx
index beb09cdc7..394ff9946 100644
--- a/src/components/ProfileCard.tsx
+++ b/src/components/ProfileCard.tsx
@@ -8,15 +8,15 @@ import {
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
+import {getModerationCauseKey} from '#/lib/moderation'
 import {type LogEvents} from '#/lib/statsig/statsig'
 import {sanitizeDisplayName} from '#/lib/strings/display-names'
 import {sanitizeHandle} from '#/lib/strings/handles'
 import {useProfileShadow} from '#/state/cache/profile-shadow'
 import {useProfileFollowMutationQueue} from '#/state/queries/profile'
 import {useSession} from '#/state/session'
-import {ProfileCardPills} from '#/view/com/profile/ProfileCard'
 import * as Toast from '#/view/com/util/Toast'
-import {UserAvatar} from '#/view/com/util/UserAvatar'
+import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
 import {atoms as a, useTheme} from '#/alf'
 import {
   Button,
@@ -27,6 +27,7 @@ import {
 import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
 import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
 import {Link as InternalLink, type LinkProps} from '#/components/Link'
+import * as Pills from '#/components/Pills'
 import {RichText} from '#/components/RichText'
 import {Text} from '#/components/Typography'
 import type * as bsky from '#/types/bsky'
@@ -35,13 +36,15 @@ export function Default({
   profile,
   moderationOpts,
   logContext = 'ProfileCard',
+  testID,
 }: {
   profile: bsky.profile.AnyProfileView
   moderationOpts: ModerationOpts
   logContext?: 'ProfileCard' | 'StarterPackProfilesList'
+  testID?: string
 }) {
   return (
-    <Link profile={profile}>
+    <Link testID={testID} profile={profile}>
       <Card
         profile={profile}
         moderationOpts={moderationOpts}
@@ -60,8 +63,6 @@ export function Card({
   moderationOpts: ModerationOpts
   logContext?: 'ProfileCard' | 'StarterPackProfilesList'
 }) {
-  const moderation = moderateProfile(profile, moderationOpts)
-
   return (
     <Outer>
       <Header>
@@ -74,10 +75,7 @@ export function Card({
         />
       </Header>
 
-      <ProfileCardPills
-        followedBy={Boolean(profile.viewer?.followedBy)}
-        moderation={moderation}
-      />
+      <Labels profile={profile} moderationOpts={moderationOpts} />
 
       <Description profile={profile} />
     </Outer>
@@ -87,7 +85,7 @@ export function Card({
 export function Outer({
   children,
 }: {
-  children: React.ReactElement | React.ReactElement[]
+  children: React.ReactNode | React.ReactNode[]
 }) {
   return <View style={[a.w_full, a.flex_1, a.gap_xs]}>{children}</View>
 }
@@ -95,7 +93,7 @@ export function Outer({
 export function Header({
   children,
 }: {
-  children: React.ReactElement | React.ReactElement[]
+  children: React.ReactNode | React.ReactNode[]
 }) {
   return <View style={[a.flex_row, a.align_center, a.gap_sm]}>{children}</View>
 }
@@ -137,10 +135,9 @@ export function Avatar({
   const moderation = moderateProfile(profile, moderationOpts)
 
   return (
-    <UserAvatar
+    <PreviewableUserAvatar
       size={40}
-      avatar={profile.avatar}
-      type={profile.associated?.labeler ? 'labeler' : 'user'}
+      profile={profile}
       moderation={moderation.ui('avatar')}
     />
   )
@@ -415,3 +412,31 @@ export function FollowButtonInner({
     </View>
   )
 }
+
+export function Labels({
+  profile,
+  moderationOpts,
+}: {
+  profile: bsky.profile.AnyProfileView
+  moderationOpts: ModerationOpts
+}) {
+  const moderation = moderateProfile(profile, moderationOpts)
+  const modui = moderation.ui('profileList')
+  const followedBy = profile.viewer?.followedBy
+
+  if (!followedBy && !modui.inform && !modui.alert) {
+    return null
+  }
+
+  return (
+    <Pills.Row style={[a.pt_xs]}>
+      {followedBy && <Pills.FollowsYou />}
+      {modui.alerts.map(alert => (
+        <Pills.Label key={getModerationCauseKey(alert)} cause={alert} />
+      ))}
+      {modui.informs.map(inform => (
+        <Pills.Label key={getModerationCauseKey(inform)} cause={inform} />
+      ))}
+    </Pills.Row>
+  )
+}
diff --git a/src/screens/Search/SearchResults.tsx b/src/screens/Search/SearchResults.tsx
index bb51d2deb..6b7a582d5 100644
--- a/src/screens/Search/SearchResults.tsx
+++ b/src/screens/Search/SearchResults.tsx
@@ -275,9 +275,7 @@ let SearchScreenUserResults = ({
       {results.length ? (
         <List
           data={results}
-          renderItem={({item}) => (
-            <ProfileCardWithFollowBtn profile={item} noBg />
-          )}
+          renderItem={({item}) => <ProfileCardWithFollowBtn profile={item} />}
           keyExtractor={item => item.did}
           desktopFixedHeight
           contentContainerStyle={{paddingBottom: 100}}
diff --git a/src/view/com/lists/ListMembers.tsx b/src/view/com/lists/ListMembers.tsx
index da32c2d48..dc18cbe5d 100644
--- a/src/view/com/lists/ListMembers.tsx
+++ b/src/view/com/lists/ListMembers.tsx
@@ -1,22 +1,23 @@
 import React, {useCallback} from 'react'
 import {Dimensions, type StyleProp, View, type ViewStyle} from 'react-native'
 import {type AppBskyGraphDefs} from '@atproto/api'
-import {msg} from '@lingui/macro'
+import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
 import {cleanError} from '#/lib/strings/errors'
 import {logger} from '#/logger'
 import {useModalControls} from '#/state/modals'
+import {useModerationOpts} from '#/state/preferences/moderation-opts'
 import {useListMembersQuery} from '#/state/queries/list-members'
 import {useSession} from '#/state/session'
-import {ProfileCard} from '#/view/com/profile/ProfileCard'
 import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
-import {Button} from '#/view/com/util/forms/Button'
 import {List, type ListRef} from '#/view/com/util/List'
 import {ProfileCardFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
 import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn'
+import {atoms as a, useTheme} from '#/alf'
+import {Button, ButtonText} from '#/components/Button'
 import {ListFooter} from '#/components/Lists'
+import * as ProfileCard from '#/components/ProfileCard'
 import type * as bsky from '#/types/bsky'
 
 const LOADING_ITEM = {_reactKey: '__loading__'}
@@ -47,11 +48,12 @@ export function ListMembers({
   headerOffset?: number
   desktopFixedHeightOffset?: number
 }) {
+  const t = useTheme()
   const {_} = useLingui()
   const [isRefreshing, setIsRefreshing] = React.useState(false)
-  const {isMobile} = useWebMediaQueries()
   const {openModal} = useModalControls()
   const {currentAccount} = useSession()
+  const moderationOpts = useModerationOpts()
 
   const {
     data,
@@ -131,23 +133,6 @@ export function ListMembers({
   // rendering
   // =
 
-  const renderMemberButton = React.useCallback(
-    (profile: bsky.profile.AnyProfileView) => {
-      if (!isOwner) {
-        return null
-      }
-      return (
-        <Button
-          testID={`user-${profile.handle}-editBtn`}
-          type="default"
-          label={_(msg({message: 'Edit', context: 'action'}))}
-          onPress={() => onPressEditMembership(profile)}
-        />
-      )
-    },
-    [isOwner, onPressEditMembership, _],
-  )
-
   const renderItem = React.useCallback(
     ({item}: {item: any}) => {
       if (item === EMPTY_ITEM) {
@@ -171,26 +156,60 @@ export function ListMembers({
       } else if (item === LOADING_ITEM) {
         return <ProfileCardFeedLoadingPlaceholder />
       }
+
+      const profile = (item as AppBskyGraphDefs.ListItemView).subject
+      if (!moderationOpts) return null
+
       return (
-        <ProfileCard
-          testID={`user-${
-            (item as AppBskyGraphDefs.ListItemView).subject.handle
-          }`}
-          profile={(item as AppBskyGraphDefs.ListItemView).subject}
-          renderButton={renderMemberButton}
-          style={{paddingHorizontal: isMobile ? 8 : 14, paddingVertical: 4}}
-          noModFilter
-        />
+        <View
+          style={[a.py_md, a.px_xl, a.border_t, t.atoms.border_contrast_low]}>
+          <ProfileCard.Link profile={profile}>
+            <ProfileCard.Outer>
+              <ProfileCard.Header>
+                <ProfileCard.Avatar
+                  profile={profile}
+                  moderationOpts={moderationOpts}
+                />
+                <ProfileCard.NameAndHandle
+                  profile={profile}
+                  moderationOpts={moderationOpts}
+                />
+                {isOwner && (
+                  <Button
+                    testID={`user-${profile.handle}-editBtn`}
+                    label={_(msg({message: 'Edit', context: 'action'}))}
+                    onPress={() => onPressEditMembership(profile)}
+                    size="small"
+                    variant="solid"
+                    color="secondary">
+                    <ButtonText>
+                      <Trans context="action">Edit</Trans>
+                    </ButtonText>
+                  </Button>
+                )}
+              </ProfileCard.Header>
+
+              <ProfileCard.Labels
+                profile={profile}
+                moderationOpts={moderationOpts}
+              />
+
+              <ProfileCard.Description profile={profile} />
+            </ProfileCard.Outer>
+          </ProfileCard.Link>
+        </View>
       )
     },
     [
-      renderMemberButton,
       renderEmptyState,
       error,
       onPressTryAgain,
       onPressRetryLoadMore,
-      isMobile,
+      moderationOpts,
+      isOwner,
+      onPressEditMembership,
       _,
+      t,
     ],
   )
 
diff --git a/src/view/com/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx
index bd09d6514..cee950703 100644
--- a/src/view/com/profile/ProfileCard.tsx
+++ b/src/view/com/profile/ProfileCard.tsx
@@ -1,307 +1,36 @@
-import React from 'react'
-import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
-import {
-  AppBskyActorDefs,
-  moderateProfile,
-  ModerationDecision,
-} from '@atproto/api'
-import {useQueryClient} from '@tanstack/react-query'
+import {View} from 'react-native'
+import {type AppBskyActorDefs} from '@atproto/api'
 
-import {usePalette} from '#/lib/hooks/usePalette'
-import {getModerationCauseKey, isJustAMute} from '#/lib/moderation'
-import {makeProfileLink} from '#/lib/routes/links'
-import {sanitizeDisplayName} from '#/lib/strings/display-names'
-import {sanitizeHandle} from '#/lib/strings/handles'
-import {s} from '#/lib/styles'
-import {useProfileShadow} from '#/state/cache/profile-shadow'
-import {Shadow} from '#/state/cache/types'
 import {useModerationOpts} from '#/state/preferences/moderation-opts'
-import {precacheProfile} from '#/state/queries/profile'
-import {useSession} from '#/state/session'
-import {atoms as a} from '#/alf'
-import {
-  KnownFollowers,
-  shouldShowKnownFollowers,
-} from '#/components/KnownFollowers'
-import * as Pills from '#/components/Pills'
-import * as bsky from '#/types/bsky'
-import {Link} from '../util/Link'
-import {Text} from '../util/text/Text'
-import {PreviewableUserAvatar} from '../util/UserAvatar'
-import {FollowButton} from './FollowButton'
-
-export function ProfileCard({
-  testID,
-  profile: profileUnshadowed,
-  noModFilter,
-  noBg,
-  noBorder,
-  renderButton,
-  onPress,
-  style,
-  showKnownFollowers,
-}: {
-  testID?: string
-  profile: bsky.profile.AnyProfileView
-  noModFilter?: boolean
-  noBg?: boolean
-  noBorder?: boolean
-  renderButton?: (
-    profile: Shadow<bsky.profile.AnyProfileView>,
-  ) => React.ReactNode
-  onPress?: () => void
-  style?: StyleProp<ViewStyle>
-  showKnownFollowers?: boolean
-}) {
-  const queryClient = useQueryClient()
-  const pal = usePalette('default')
-  const profile = useProfileShadow(profileUnshadowed)
-  const moderationOpts = useModerationOpts()
-  const isLabeler = profile?.associated?.labeler
-
-  const onBeforePress = React.useCallback(() => {
-    onPress?.()
-    precacheProfile(queryClient, profile)
-  }, [onPress, profile, queryClient])
-
-  if (!moderationOpts) {
-    return null
-  }
-  const moderation = moderateProfile(profile, moderationOpts)
-  const modui = moderation.ui('profileList')
-  if (!noModFilter && modui.filter && !isJustAMute(modui)) {
-    return null
-  }
-
-  const knownFollowersVisible =
-    showKnownFollowers &&
-    shouldShowKnownFollowers(profile.viewer?.knownFollowers) &&
-    moderationOpts
-  const hasDescription = 'description' in profile
-
-  return (
-    <Link
-      testID={testID}
-      style={[
-        styles.outer,
-        pal.border,
-        noBorder && styles.outerNoBorder,
-        !noBg && pal.view,
-        style,
-      ]}
-      href={makeProfileLink(profile)}
-      title={profile.handle}
-      asAnchor
-      onBeforePress={onBeforePress}
-      anchorNoUnderline>
-      <View style={styles.layout}>
-        <View style={styles.layoutAvi}>
-          <PreviewableUserAvatar
-            size={40}
-            profile={profile}
-            moderation={moderation.ui('avatar')}
-            type={isLabeler ? 'labeler' : 'user'}
-          />
-        </View>
-        <View style={styles.layoutContent}>
-          <Text
-            emoji
-            type="lg"
-            style={[s.bold, pal.text, a.self_start]}
-            numberOfLines={1}
-            lineHeight={1.2}>
-            {sanitizeDisplayName(
-              profile.displayName || sanitizeHandle(profile.handle),
-              moderation.ui('displayName'),
-            )}
-          </Text>
-          <Text emoji type="md" style={[pal.textLight]} numberOfLines={1}>
-            {sanitizeHandle(profile.handle, '@')}
-          </Text>
-          <ProfileCardPills
-            followedBy={!!profile.viewer?.followedBy}
-            moderation={moderation}
-          />
-          {!!profile.viewer?.followedBy && <View style={s.flexRow} />}
-        </View>
-        {renderButton && !isLabeler ? (
-          <View style={styles.layoutButton}>{renderButton(profile)}</View>
-        ) : undefined}
-      </View>
-      {hasDescription || knownFollowersVisible ? (
-        <View style={styles.details}>
-          {hasDescription && profile.description ? (
-            <Text emoji style={pal.text} numberOfLines={4}>
-              {profile.description as string}
-            </Text>
-          ) : null}
-          {knownFollowersVisible ? (
-            <View
-              style={[
-                a.flex_row,
-                a.align_center,
-                a.gap_sm,
-                !!hasDescription && a.mt_md,
-              ]}>
-              <KnownFollowers
-                minimal
-                profile={profile}
-                moderationOpts={moderationOpts}
-              />
-            </View>
-          ) : null}
-        </View>
-      ) : null}
-    </Link>
-  )
-}
-
-export function ProfileCardPills({
-  followedBy,
-  moderation,
-}: {
-  followedBy: boolean
-  moderation: ModerationDecision
-}) {
-  const modui = moderation.ui('profileList')
-  if (!followedBy && !modui.inform && !modui.alert) {
-    return null
-  }
-
-  return (
-    <Pills.Row style={[a.pt_xs]}>
-      {followedBy && <Pills.FollowsYou />}
-      {modui.alerts.map(alert => (
-        <Pills.Label key={getModerationCauseKey(alert)} cause={alert} />
-      ))}
-      {modui.informs.map(inform => (
-        <Pills.Label key={getModerationCauseKey(inform)} cause={inform} />
-      ))}
-    </Pills.Row>
-  )
-}
+import {atoms as a, useTheme} from '#/alf'
+import * as ProfileCard from '#/components/ProfileCard'
 
 export function ProfileCardWithFollowBtn({
   profile,
-  noBg,
   noBorder,
-  onPress,
-  onFollow,
   logContext = 'ProfileCard',
-  showKnownFollowers,
 }: {
   profile: AppBskyActorDefs.ProfileView
-  noBg?: boolean
   noBorder?: boolean
-  onPress?: () => void
-  onFollow?: () => void
   logContext?: 'ProfileCard' | 'StarterPackProfilesList'
-  showKnownFollowers?: boolean
 }) {
-  const {currentAccount} = useSession()
-  const isMe = profile.did === currentAccount?.did
+  const t = useTheme()
+  const moderationOpts = useModerationOpts()
+
+  if (!moderationOpts) return null
 
   return (
-    <ProfileCard
-      profile={profile}
-      noBg={noBg}
-      noBorder={noBorder}
-      renderButton={
-        isMe
-          ? undefined
-          : profileShadow => (
-              <FollowButton
-                profile={profileShadow}
-                logContext={logContext}
-                onFollow={onFollow}
-              />
-            )
-      }
-      onPress={onPress}
-      showKnownFollowers={!isMe && showKnownFollowers}
-    />
+    <View
+      style={[
+        a.py_md,
+        a.px_xl,
+        !noBorder && [a.border_t, t.atoms.border_contrast_low],
+      ]}>
+      <ProfileCard.Default
+        profile={profile}
+        moderationOpts={moderationOpts}
+        logContext={logContext}
+      />
+    </View>
   )
 }
-
-const styles = StyleSheet.create({
-  outer: {
-    borderTopWidth: StyleSheet.hairlineWidth,
-    paddingHorizontal: 6,
-    paddingVertical: 4,
-  },
-  outerNoBorder: {
-    borderTopWidth: 0,
-  },
-  layout: {
-    flexDirection: 'row',
-    alignItems: 'center',
-  },
-  layoutAvi: {
-    alignSelf: 'flex-start',
-    width: 54,
-    paddingLeft: 4,
-    paddingTop: 10,
-  },
-  avi: {
-    width: 40,
-    height: 40,
-    borderRadius: 20,
-    resizeMode: 'cover',
-  },
-  layoutContent: {
-    flex: 1,
-    paddingRight: 10,
-    paddingTop: 10,
-    paddingBottom: 10,
-  },
-  layoutButton: {
-    paddingRight: 10,
-  },
-  details: {
-    justifyContent: 'center',
-    paddingLeft: 54,
-    paddingRight: 10,
-    paddingBottom: 10,
-  },
-  pills: {
-    alignItems: 'flex-start',
-    flexDirection: 'row',
-    flexWrap: 'wrap',
-    columnGap: 6,
-    rowGap: 2,
-  },
-  pill: {
-    borderRadius: 4,
-    paddingHorizontal: 6,
-    paddingVertical: 2,
-    justifyContent: 'center',
-  },
-  btn: {
-    paddingVertical: 7,
-    borderRadius: 50,
-    marginLeft: 6,
-    paddingHorizontal: 14,
-  },
-
-  followedBy: {
-    flexDirection: 'row',
-    paddingLeft: 54,
-    paddingRight: 20,
-    marginBottom: 10,
-    marginTop: -6,
-  },
-  followedByAviContainer: {
-    width: 24,
-    height: 36,
-  },
-  followedByAvi: {
-    width: 36,
-    height: 36,
-    borderRadius: 18,
-    padding: 2,
-  },
-  followsByDesc: {
-    flex: 1,
-    paddingRight: 10,
-  },
-})
diff --git a/src/view/screens/DebugMod.tsx b/src/view/screens/DebugMod.tsx
index 9774c644c..c3a82ac8e 100644
--- a/src/view/screens/DebugMod.tsx
+++ b/src/view/screens/DebugMod.tsx
@@ -1,29 +1,32 @@
-/* eslint-disable no-restricted-imports */
 import React from 'react'
 import {View} from 'react-native'
 import {
-  AppBskyActorDefs,
-  AppBskyFeedDefs,
-  AppBskyFeedPost,
-  ComAtprotoLabelDefs,
+  type AppBskyActorDefs,
+  type AppBskyFeedDefs,
+  type AppBskyFeedPost,
+  type ComAtprotoLabelDefs,
   interpretLabelValueDefinition,
-  LabelPreference,
+  type LabelPreference,
   LABELS,
   mock,
   moderatePost,
   moderateProfile,
-  ModerationBehavior,
-  ModerationDecision,
-  ModerationOpts,
+  type ModerationBehavior,
+  type ModerationDecision,
+  type ModerationOpts,
   RichText,
 } from '@atproto/api'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
 import {useGlobalLabelStrings} from '#/lib/moderation/useGlobalLabelStrings'
-import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
+import {
+  type CommonNavigatorParams,
+  type NativeStackScreenProps,
+} from '#/lib/routes/types'
+import {useModerationOpts} from '#/state/preferences/moderation-opts'
 import {moderationOptsOverrideContext} from '#/state/preferences/moderation-opts'
-import {FeedNotification} from '#/state/queries/notifications/types'
+import {type FeedNotification} from '#/state/queries/notifications/types'
 import {
   groupNotifications,
   shouldFilterNotif,
@@ -42,12 +45,12 @@ import {
   ChevronTop_Stroke2_Corner0_Rounded as ChevronTop,
 } from '#/components/icons/Chevron'
 import * as Layout from '#/components/Layout'
+import * as ProfileCard from '#/components/ProfileCard'
 import {H1, H3, P, Text} from '#/components/Typography'
 import {ScreenHider} from '../../components/moderation/ScreenHider'
 import {NotificationFeedItem} from '../com/notifications/NotificationFeedItem'
 import {PostThreadItem} from '../com/post-thread/PostThreadItem'
 import {PostFeedItem} from '../com/posts/PostFeedItem'
-import {ProfileCard} from '../com/profile/ProfileCard'
 
 const LABEL_VALUES: (keyof typeof LABELS)[] = Object.keys(
   LABELS,
@@ -890,6 +893,9 @@ function MockAccountCard({
   moderation: ModerationDecision
 }) {
   const t = useTheme()
+  const moderationOpts = useModerationOpts()
+
+  if (!moderationOpts) return null
 
   if (moderation.ui('profileList').filter) {
     return (
@@ -899,7 +905,7 @@ function MockAccountCard({
     )
   }
 
-  return <ProfileCard profile={profile} />
+  return <ProfileCard.Card profile={profile} moderationOpts={moderationOpts} />
 }
 
 function MockAccountScreen({
diff --git a/src/view/screens/ModerationBlockedAccounts.tsx b/src/view/screens/ModerationBlockedAccounts.tsx
index c2f87c086..cefa29f6c 100644
--- a/src/view/screens/ModerationBlockedAccounts.tsx
+++ b/src/view/screens/ModerationBlockedAccounts.tsx
@@ -6,35 +6,38 @@ import {
   StyleSheet,
   View,
 } from 'react-native'
-import {AppBskyActorDefs as ActorDefs} from '@atproto/api'
+import {type AppBskyActorDefs as ActorDefs} from '@atproto/api'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useFocusEffect} from '@react-navigation/native'
-import {NativeStackScreenProps} from '@react-navigation/native-stack'
+import {type NativeStackScreenProps} from '@react-navigation/native-stack'
 
 import {usePalette} from '#/lib/hooks/usePalette'
 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
-import {CommonNavigatorParams} from '#/lib/routes/types'
+import {type CommonNavigatorParams} from '#/lib/routes/types'
 import {cleanError} from '#/lib/strings/errors'
 import {logger} from '#/logger'
+import {useModerationOpts} from '#/state/preferences/moderation-opts'
 import {useMyBlockedAccountsQuery} from '#/state/queries/my-blocked-accounts'
 import {useSetMinimalShellMode} from '#/state/shell'
-import {ProfileCard} from '#/view/com/profile/ProfileCard'
 import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
 import {Text} from '#/view/com/util/text/Text'
 import {ViewHeader} from '#/view/com/util/ViewHeader'
-import {atoms as a} from '#/alf'
+import {atoms as a, useTheme} from '#/alf'
 import * as Layout from '#/components/Layout'
+import * as ProfileCard from '#/components/ProfileCard'
 
 type Props = NativeStackScreenProps<
   CommonNavigatorParams,
   'ModerationBlockedAccounts'
 >
 export function ModerationBlockedAccounts({}: Props) {
+  const t = useTheme()
   const pal = usePalette('default')
   const {_} = useLingui()
   const setMinimalShellMode = useSetMinimalShellMode()
   const {isTabletOrDesktop} = useWebMediaQueries()
+  const moderationOpts = useModerationOpts()
 
   const [isPTRing, setIsPTRing] = React.useState(false)
   const {
@@ -87,14 +90,20 @@ export function ModerationBlockedAccounts({}: Props) {
   }: {
     item: ActorDefs.ProfileView
     index: number
-  }) => (
-    <ProfileCard
-      testID={`blockedAccount-${index}`}
-      key={item.did}
-      profile={item}
-      noModFilter
-    />
-  )
+  }) => {
+    if (!moderationOpts) return null
+    return (
+      <View
+        style={[a.py_md, a.px_xl, a.border_t, t.atoms.border_contrast_low]}
+        key={item.did}>
+        <ProfileCard.Default
+          testID={`blockedAccount-${index}`}
+          profile={item}
+          moderationOpts={moderationOpts}
+        />
+      </View>
+    )
+  }
   return (
     <Layout.Screen testID="blockedAccountsScreen">
       <Layout.Center style={[a.flex_1, {paddingBottom: 100}]}>
diff --git a/src/view/screens/ModerationMutedAccounts.tsx b/src/view/screens/ModerationMutedAccounts.tsx
index 059985631..f49337b7c 100644
--- a/src/view/screens/ModerationMutedAccounts.tsx
+++ b/src/view/screens/ModerationMutedAccounts.tsx
@@ -6,35 +6,38 @@ import {
   StyleSheet,
   View,
 } from 'react-native'
-import {AppBskyActorDefs as ActorDefs} from '@atproto/api'
+import {type AppBskyActorDefs as ActorDefs} from '@atproto/api'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useFocusEffect} from '@react-navigation/native'
-import {NativeStackScreenProps} from '@react-navigation/native-stack'
+import {type NativeStackScreenProps} from '@react-navigation/native-stack'
 
 import {usePalette} from '#/lib/hooks/usePalette'
 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
-import {CommonNavigatorParams} from '#/lib/routes/types'
+import {type CommonNavigatorParams} from '#/lib/routes/types'
 import {cleanError} from '#/lib/strings/errors'
 import {logger} from '#/logger'
+import {useModerationOpts} from '#/state/preferences/moderation-opts'
 import {useMyMutedAccountsQuery} from '#/state/queries/my-muted-accounts'
 import {useSetMinimalShellMode} from '#/state/shell'
-import {ProfileCard} from '#/view/com/profile/ProfileCard'
 import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
 import {Text} from '#/view/com/util/text/Text'
 import {ViewHeader} from '#/view/com/util/ViewHeader'
-import {atoms as a} from '#/alf'
+import {atoms as a, useTheme} from '#/alf'
 import * as Layout from '#/components/Layout'
+import * as ProfileCard from '#/components/ProfileCard'
 
 type Props = NativeStackScreenProps<
   CommonNavigatorParams,
   'ModerationMutedAccounts'
 >
 export function ModerationMutedAccounts({}: Props) {
+  const t = useTheme()
   const pal = usePalette('default')
   const {_} = useLingui()
   const setMinimalShellMode = useSetMinimalShellMode()
   const {isTabletOrDesktop} = useWebMediaQueries()
+  const moderationOpts = useModerationOpts()
 
   const [isPTRing, setIsPTRing] = React.useState(false)
   const {
@@ -87,14 +90,34 @@ export function ModerationMutedAccounts({}: Props) {
   }: {
     item: ActorDefs.ProfileView
     index: number
-  }) => (
-    <ProfileCard
-      testID={`mutedAccount-${index}`}
-      key={item.did}
-      profile={item}
-      noModFilter
-    />
-  )
+  }) => {
+    if (!moderationOpts) return null
+    return (
+      <View
+        style={[a.py_md, a.px_xl, a.border_t, t.atoms.border_contrast_low]}
+        key={item.did}>
+        <ProfileCard.Link profile={item} testID={`mutedAccount-${index}`}>
+          <ProfileCard.Outer>
+            <ProfileCard.Header>
+              <ProfileCard.Avatar
+                profile={item}
+                moderationOpts={moderationOpts}
+              />
+              <ProfileCard.NameAndHandle
+                profile={item}
+                moderationOpts={moderationOpts}
+              />
+            </ProfileCard.Header>
+            <ProfileCard.Labels
+              profile={item}
+              moderationOpts={moderationOpts}
+            />
+            <ProfileCard.Description profile={item} />
+          </ProfileCard.Outer>
+        </ProfileCard.Link>
+      </View>
+    )
+  }
   return (
     <Layout.Screen testID="mutedAccountsScreen">
       <ViewHeader title={_(msg`Muted Accounts`)} showOnDesktop />