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/FollowButton.tsx94
-rw-r--r--src/view/com/profile/ProfileCard.tsx223
-rw-r--r--src/view/com/profile/ProfileFollowers.tsx2
-rw-r--r--src/view/com/profile/ProfileFollows.tsx2
-rw-r--r--src/view/com/profile/ProfileHeader.tsx898
5 files changed, 604 insertions, 615 deletions
diff --git a/src/view/com/profile/FollowButton.tsx b/src/view/com/profile/FollowButton.tsx
index fcb2225da..6f6286e69 100644
--- a/src/view/com/profile/FollowButton.tsx
+++ b/src/view/com/profile/FollowButton.tsx
@@ -6,56 +6,54 @@ import {useStores} from 'state/index'
 import * as Toast from '../util/Toast'
 import {FollowState} from 'state/models/cache/my-follows'
 
-export const FollowButton = observer(
-  ({
-    unfollowedType = 'inverted',
-    followedType = 'default',
-    did,
-    onToggleFollow,
-  }: {
-    unfollowedType?: ButtonType
-    followedType?: ButtonType
-    did: string
-    onToggleFollow?: (v: boolean) => void
-  }) => {
-    const store = useStores()
-    const followState = store.me.follows.getFollowState(did)
+export const FollowButton = observer(function FollowButtonImpl({
+  unfollowedType = 'inverted',
+  followedType = 'default',
+  did,
+  onToggleFollow,
+}: {
+  unfollowedType?: ButtonType
+  followedType?: ButtonType
+  did: string
+  onToggleFollow?: (v: boolean) => void
+}) {
+  const store = useStores()
+  const followState = store.me.follows.getFollowState(did)
 
-    if (followState === FollowState.Unknown) {
-      return <View />
-    }
+  if (followState === FollowState.Unknown) {
+    return <View />
+  }
 
-    const onToggleFollowInner = async () => {
-      const updatedFollowState = await store.me.follows.fetchFollowState(did)
-      if (updatedFollowState === FollowState.Following) {
-        try {
-          await store.agent.deleteFollow(store.me.follows.getFollowUri(did))
-          store.me.follows.removeFollow(did)
-          onToggleFollow?.(false)
-        } catch (e: any) {
-          store.log.error('Failed to delete follow', e)
-          Toast.show('An issue occurred, please try again.')
-        }
-      } else if (updatedFollowState === FollowState.NotFollowing) {
-        try {
-          const res = await store.agent.follow(did)
-          store.me.follows.addFollow(did, res.uri)
-          onToggleFollow?.(true)
-        } catch (e: any) {
-          store.log.error('Failed to create follow', e)
-          Toast.show('An issue occurred, please try again.')
-        }
+  const onToggleFollowInner = async () => {
+    const updatedFollowState = await store.me.follows.fetchFollowState(did)
+    if (updatedFollowState === FollowState.Following) {
+      try {
+        await store.agent.deleteFollow(store.me.follows.getFollowUri(did))
+        store.me.follows.removeFollow(did)
+        onToggleFollow?.(false)
+      } catch (e: any) {
+        store.log.error('Failed to delete follow', e)
+        Toast.show('An issue occurred, please try again.')
+      }
+    } else if (updatedFollowState === FollowState.NotFollowing) {
+      try {
+        const res = await store.agent.follow(did)
+        store.me.follows.addFollow(did, res.uri)
+        onToggleFollow?.(true)
+      } catch (e: any) {
+        store.log.error('Failed to create follow', e)
+        Toast.show('An issue occurred, please try again.')
       }
     }
+  }
 
-    return (
-      <Button
-        type={
-          followState === FollowState.Following ? followedType : unfollowedType
-        }
-        onPress={onToggleFollowInner}
-        label={followState === FollowState.Following ? 'Unfollow' : 'Follow'}
-      />
-    )
-  },
-)
+  return (
+    <Button
+      type={
+        followState === FollowState.Following ? followedType : unfollowedType
+      }
+      onPress={onToggleFollowInner}
+      label={followState === FollowState.Following ? 'Unfollow' : 'Follow'}
+    />
+  )
+})
diff --git a/src/view/com/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx
index 771785ee9..e0c8ad21a 100644
--- a/src/view/com/profile/ProfileCard.tsx
+++ b/src/view/com/profile/ProfileCard.tsx
@@ -22,89 +22,82 @@ import {
   getModerationCauseKey,
 } from 'lib/moderation'
 
-export const ProfileCard = observer(
-  ({
-    testID,
-    profile,
-    noBg,
-    noBorder,
-    followers,
-    renderButton,
-  }: {
-    testID?: string
-    profile: AppBskyActorDefs.ProfileViewBasic
-    noBg?: boolean
-    noBorder?: boolean
-    followers?: AppBskyActorDefs.ProfileView[] | undefined
-    renderButton?: (
-      profile: AppBskyActorDefs.ProfileViewBasic,
-    ) => React.ReactNode
-  }) => {
-    const store = useStores()
-    const pal = usePalette('default')
+export const ProfileCard = observer(function ProfileCardImpl({
+  testID,
+  profile,
+  noBg,
+  noBorder,
+  followers,
+  renderButton,
+}: {
+  testID?: string
+  profile: AppBskyActorDefs.ProfileViewBasic
+  noBg?: boolean
+  noBorder?: boolean
+  followers?: AppBskyActorDefs.ProfileView[] | undefined
+  renderButton?: (profile: AppBskyActorDefs.ProfileViewBasic) => React.ReactNode
+}) {
+  const store = useStores()
+  const pal = usePalette('default')
 
-    const moderation = moderateProfile(
-      profile,
-      store.preferences.moderationOpts,
-    )
+  const moderation = moderateProfile(profile, store.preferences.moderationOpts)
 
-    return (
-      <Link
-        testID={testID}
-        style={[
-          styles.outer,
-          pal.border,
-          noBorder && styles.outerNoBorder,
-          !noBg && pal.view,
-        ]}
-        href={makeProfileLink(profile)}
-        title={profile.handle}
-        asAnchor
-        anchorNoUnderline>
-        <View style={styles.layout}>
-          <View style={styles.layoutAvi}>
-            <UserAvatar
-              size={40}
-              avatar={profile.avatar}
-              moderation={moderation.avatar}
-            />
-          </View>
-          <View style={styles.layoutContent}>
-            <Text
-              type="lg"
-              style={[s.bold, pal.text]}
-              numberOfLines={1}
-              lineHeight={1.2}>
-              {sanitizeDisplayName(
-                profile.displayName || sanitizeHandle(profile.handle),
-                moderation.profile,
-              )}
-            </Text>
-            <Text 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 ? (
-            <View style={styles.layoutButton}>{renderButton(profile)}</View>
-          ) : undefined}
+  return (
+    <Link
+      testID={testID}
+      style={[
+        styles.outer,
+        pal.border,
+        noBorder && styles.outerNoBorder,
+        !noBg && pal.view,
+      ]}
+      href={makeProfileLink(profile)}
+      title={profile.handle}
+      asAnchor
+      anchorNoUnderline>
+      <View style={styles.layout}>
+        <View style={styles.layoutAvi}>
+          <UserAvatar
+            size={40}
+            avatar={profile.avatar}
+            moderation={moderation.avatar}
+          />
         </View>
-        {profile.description ? (
-          <View style={styles.details}>
-            <Text style={pal.text} numberOfLines={4}>
-              {profile.description as string}
-            </Text>
-          </View>
+        <View style={styles.layoutContent}>
+          <Text
+            type="lg"
+            style={[s.bold, pal.text]}
+            numberOfLines={1}
+            lineHeight={1.2}>
+            {sanitizeDisplayName(
+              profile.displayName || sanitizeHandle(profile.handle),
+              moderation.profile,
+            )}
+          </Text>
+          <Text 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 ? (
+          <View style={styles.layoutButton}>{renderButton(profile)}</View>
         ) : undefined}
-        <FollowersList followers={followers} />
-      </Link>
-    )
-  },
-)
+      </View>
+      {profile.description ? (
+        <View style={styles.details}>
+          <Text style={pal.text} numberOfLines={4}>
+            {profile.description as string}
+          </Text>
+        </View>
+      ) : undefined}
+      <FollowersList followers={followers} />
+    </Link>
+  )
+})
 
 function ProfileCardPills({
   followedBy,
@@ -146,45 +139,47 @@ function ProfileCardPills({
   )
 }
 
-const FollowersList = observer(
-  ({followers}: {followers?: AppBskyActorDefs.ProfileView[] | undefined}) => {
-    const store = useStores()
-    const pal = usePalette('default')
-    if (!followers?.length) {
-      return null
-    }
+const FollowersList = observer(function FollowersListImpl({
+  followers,
+}: {
+  followers?: AppBskyActorDefs.ProfileView[] | undefined
+}) {
+  const store = useStores()
+  const pal = usePalette('default')
+  if (!followers?.length) {
+    return null
+  }
 
-    const followersWithMods = followers
-      .map(f => ({
-        f,
-        mod: moderateProfile(f, store.preferences.moderationOpts),
-      }))
-      .filter(({mod}) => !mod.account.filter)
+  const followersWithMods = followers
+    .map(f => ({
+      f,
+      mod: moderateProfile(f, store.preferences.moderationOpts),
+    }))
+    .filter(({mod}) => !mod.account.filter)
 
-    return (
-      <View style={styles.followedBy}>
-        <Text
-          type="sm"
-          style={[styles.followsByDesc, pal.textLight]}
-          numberOfLines={2}
-          lineHeight={1.2}>
-          Followed by{' '}
-          {followersWithMods.map(({f}) => f.displayName || f.handle).join(', ')}
-        </Text>
-        {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} />
-            </View>
+  return (
+    <View style={styles.followedBy}>
+      <Text
+        type="sm"
+        style={[styles.followsByDesc, pal.textLight]}
+        numberOfLines={2}
+        lineHeight={1.2}>
+        Followed by{' '}
+        {followersWithMods.map(({f}) => f.displayName || f.handle).join(', ')}
+      </Text>
+      {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} />
           </View>
-        ))}
-      </View>
-    )
-  },
-)
+        </View>
+      ))}
+    </View>
+  )
+})
 
 export const ProfileCardWithFollowBtn = observer(
-  ({
+  function ProfileCardWithFollowBtnImpl({
     profile,
     noBg,
     noBorder,
@@ -194,7 +189,7 @@ export const ProfileCardWithFollowBtn = observer(
     noBg?: boolean
     noBorder?: boolean
     followers?: AppBskyActorDefs.ProfileView[] | undefined
-  }) => {
+  }) {
     const store = useStores()
     const isMe = store.me.did === profile.did
 
diff --git a/src/view/com/profile/ProfileFollowers.tsx b/src/view/com/profile/ProfileFollowers.tsx
index aeb2fcba9..beb9609b6 100644
--- a/src/view/com/profile/ProfileFollowers.tsx
+++ b/src/view/com/profile/ProfileFollowers.tsx
@@ -78,6 +78,8 @@ export const ProfileFollowers = observer(function ProfileFollowers({
       onEndReached={onEndReached}
       renderItem={renderItem}
       initialNumToRender={15}
+      // FIXME(dan)
+      // eslint-disable-next-line react/no-unstable-nested-components
       ListFooterComponent={() => (
         <View style={styles.footer}>
           {view.isLoading && <ActivityIndicator />}
diff --git a/src/view/com/profile/ProfileFollows.tsx b/src/view/com/profile/ProfileFollows.tsx
index 0632fac02..22722ee63 100644
--- a/src/view/com/profile/ProfileFollows.tsx
+++ b/src/view/com/profile/ProfileFollows.tsx
@@ -75,6 +75,8 @@ export const ProfileFollows = observer(function ProfileFollows({
       onEndReached={onEndReached}
       renderItem={renderItem}
       initialNumToRender={15}
+      // FIXME(dan)
+      // eslint-disable-next-line react/no-unstable-nested-components
       ListFooterComponent={() => (
         <View style={styles.footer}>
           {view.isLoading && <ActivityIndicator />}
diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx
index 1c683ab9a..b52d338aa 100644
--- a/src/view/com/profile/ProfileHeader.tsx
+++ b/src/view/com/profile/ProfileHeader.tsx
@@ -45,510 +45,502 @@ interface Props {
   hideBackButton?: boolean
 }
 
-export const ProfileHeader = observer(
-  ({view, onRefreshAll, hideBackButton = false}: Props) => {
-    const pal = usePalette('default')
+export const ProfileHeader = observer(function ProfileHeaderImpl({
+  view,
+  onRefreshAll,
+  hideBackButton = false,
+}: Props) {
+  const pal = usePalette('default')
 
-    // loading
-    // =
-    if (!view || !view.hasLoaded) {
-      return (
-        <View style={pal.view}>
-          <LoadingPlaceholder width="100%" height={120} />
-          <View
-            style={[
-              pal.view,
-              {borderColor: pal.colors.background},
-              styles.avi,
-            ]}>
-            <LoadingPlaceholder width={80} height={80} style={styles.br40} />
+  // loading
+  // =
+  if (!view || !view.hasLoaded) {
+    return (
+      <View style={pal.view}>
+        <LoadingPlaceholder width="100%" height={120} />
+        <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={100} height={31} style={styles.br50} />
           </View>
-          <View style={styles.content}>
-            <View style={[styles.buttonsLine]}>
-              <LoadingPlaceholder width={100} height={31} style={styles.br50} />
-            </View>
-            <View>
-              <Text type="title-2xl" style={[pal.text, styles.title]}>
-                {sanitizeDisplayName(
-                  view.displayName || sanitizeHandle(view.handle),
-                )}
-              </Text>
-            </View>
+          <View>
+            <Text type="title-2xl" style={[pal.text, styles.title]}>
+              {sanitizeDisplayName(
+                view.displayName || sanitizeHandle(view.handle),
+              )}
+            </Text>
           </View>
         </View>
-      )
-    }
-
-    // error
-    // =
-    if (view.hasError) {
-      return (
-        <View testID="profileHeaderHasError">
-          <Text>{view.error}</Text>
-        </View>
-      )
-    }
+      </View>
+    )
+  }
 
-    // loaded
-    // =
+  // error
+  // =
+  if (view.hasError) {
     return (
-      <ProfileHeaderLoaded
-        view={view}
-        onRefreshAll={onRefreshAll}
-        hideBackButton={hideBackButton}
-      />
+      <View testID="profileHeaderHasError">
+        <Text>{view.error}</Text>
+      </View>
     )
-  },
-)
+  }
 
-const ProfileHeaderLoaded = observer(
-  ({view, onRefreshAll, hideBackButton = false}: Props) => {
-    const pal = usePalette('default')
-    const palInverted = usePalette('inverted')
-    const store = useStores()
-    const navigation = useNavigation<NavigationProp>()
-    const {track} = useAnalytics()
-    const invalidHandle = isInvalidHandle(view.handle)
-    const {isDesktop} = useWebMediaQueries()
+  // loaded
+  // =
+  return (
+    <ProfileHeaderLoaded
+      view={view}
+      onRefreshAll={onRefreshAll}
+      hideBackButton={hideBackButton}
+    />
+  )
+})
 
-    const onPressBack = React.useCallback(() => {
-      navigation.goBack()
-    }, [navigation])
+const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
+  view,
+  onRefreshAll,
+  hideBackButton = false,
+}: Props) {
+  const pal = usePalette('default')
+  const palInverted = usePalette('inverted')
+  const store = useStores()
+  const navigation = useNavigation<NavigationProp>()
+  const {track} = useAnalytics()
+  const invalidHandle = isInvalidHandle(view.handle)
+  const {isDesktop} = useWebMediaQueries()
 
-    const onPressAvi = React.useCallback(() => {
-      if (
-        view.avatar &&
-        !(view.moderation.avatar.blur && view.moderation.avatar.noOverride)
-      ) {
-        store.shell.openLightbox(new ProfileImageLightbox(view))
-      }
-    }, [store, view])
+  const onPressBack = React.useCallback(() => {
+    navigation.goBack()
+  }, [navigation])
 
-    const onPressToggleFollow = React.useCallback(() => {
-      track(
-        view.viewer.following
-          ? 'ProfileHeader:FollowButtonClicked'
-          : 'ProfileHeader:UnfollowButtonClicked',
-      )
-      view?.toggleFollowing().then(
-        () => {
-          Toast.show(
-            `${
-              view.viewer.following ? 'Following' : 'No longer following'
-            } ${sanitizeDisplayName(view.displayName || view.handle)}`,
-          )
-        },
-        err => store.log.error('Failed to toggle follow', err),
-      )
-    }, [track, view, store.log])
+  const onPressAvi = React.useCallback(() => {
+    if (
+      view.avatar &&
+      !(view.moderation.avatar.blur && view.moderation.avatar.noOverride)
+    ) {
+      store.shell.openLightbox(new ProfileImageLightbox(view))
+    }
+  }, [store, view])
 
-    const onPressEditProfile = React.useCallback(() => {
-      track('ProfileHeader:EditProfileButtonClicked')
-      store.shell.openModal({
-        name: 'edit-profile',
-        profileView: view,
-        onUpdate: onRefreshAll,
-      })
-    }, [track, store, view, onRefreshAll])
+  const onPressToggleFollow = React.useCallback(() => {
+    track(
+      view.viewer.following
+        ? 'ProfileHeader:FollowButtonClicked'
+        : 'ProfileHeader:UnfollowButtonClicked',
+    )
+    view?.toggleFollowing().then(
+      () => {
+        Toast.show(
+          `${
+            view.viewer.following ? 'Following' : 'No longer following'
+          } ${sanitizeDisplayName(view.displayName || view.handle)}`,
+        )
+      },
+      err => store.log.error('Failed to toggle follow', err),
+    )
+  }, [track, view, store.log])
 
-    const onPressFollowers = React.useCallback(() => {
-      track('ProfileHeader:FollowersButtonClicked')
-      navigate('ProfileFollowers', {
-        name: isInvalidHandle(view.handle) ? view.did : view.handle,
-      })
-      store.shell.closeAllActiveElements() // for when used in the profile preview modal
-    }, [track, view, store.shell])
+  const onPressEditProfile = React.useCallback(() => {
+    track('ProfileHeader:EditProfileButtonClicked')
+    store.shell.openModal({
+      name: 'edit-profile',
+      profileView: view,
+      onUpdate: onRefreshAll,
+    })
+  }, [track, store, view, onRefreshAll])
 
-    const onPressFollows = React.useCallback(() => {
-      track('ProfileHeader:FollowsButtonClicked')
-      navigate('ProfileFollows', {
-        name: isInvalidHandle(view.handle) ? view.did : view.handle,
-      })
-      store.shell.closeAllActiveElements() // for when used in the profile preview modal
-    }, [track, view, store.shell])
+  const onPressFollowers = React.useCallback(() => {
+    track('ProfileHeader:FollowersButtonClicked')
+    navigate('ProfileFollowers', {
+      name: isInvalidHandle(view.handle) ? view.did : view.handle,
+    })
+    store.shell.closeAllActiveElements() // for when used in the profile preview modal
+  }, [track, view, store.shell])
 
-    const onPressShare = React.useCallback(() => {
-      track('ProfileHeader:ShareButtonClicked')
-      const url = toShareUrl(makeProfileLink(view))
-      shareUrl(url)
-    }, [track, view])
+  const onPressFollows = React.useCallback(() => {
+    track('ProfileHeader:FollowsButtonClicked')
+    navigate('ProfileFollows', {
+      name: isInvalidHandle(view.handle) ? view.did : view.handle,
+    })
+    store.shell.closeAllActiveElements() // for when used in the profile preview modal
+  }, [track, view, store.shell])
 
-    const onPressAddRemoveLists = React.useCallback(() => {
-      track('ProfileHeader:AddToListsButtonClicked')
-      store.shell.openModal({
-        name: 'list-add-remove-user',
-        subject: view.did,
-        displayName: view.displayName || view.handle,
-      })
-    }, [track, view, store])
+  const onPressShare = React.useCallback(() => {
+    track('ProfileHeader:ShareButtonClicked')
+    const url = toShareUrl(makeProfileLink(view))
+    shareUrl(url)
+  }, [track, view])
 
-    const onPressMuteAccount = React.useCallback(async () => {
-      track('ProfileHeader:MuteAccountButtonClicked')
-      try {
-        await view.muteAccount()
-        Toast.show('Account muted')
-      } catch (e: any) {
-        store.log.error('Failed to mute account', e)
-        Toast.show(`There was an issue! ${e.toString()}`)
-      }
-    }, [track, view, store])
+  const onPressAddRemoveLists = React.useCallback(() => {
+    track('ProfileHeader:AddToListsButtonClicked')
+    store.shell.openModal({
+      name: 'list-add-remove-user',
+      subject: view.did,
+      displayName: view.displayName || view.handle,
+    })
+  }, [track, view, store])
 
-    const onPressUnmuteAccount = React.useCallback(async () => {
-      track('ProfileHeader:UnmuteAccountButtonClicked')
-      try {
-        await view.unmuteAccount()
-        Toast.show('Account unmuted')
-      } catch (e: any) {
-        store.log.error('Failed to unmute account', e)
-        Toast.show(`There was an issue! ${e.toString()}`)
-      }
-    }, [track, view, store])
+  const onPressMuteAccount = React.useCallback(async () => {
+    track('ProfileHeader:MuteAccountButtonClicked')
+    try {
+      await view.muteAccount()
+      Toast.show('Account muted')
+    } catch (e: any) {
+      store.log.error('Failed to mute account', e)
+      Toast.show(`There was an issue! ${e.toString()}`)
+    }
+  }, [track, view, store])
 
-    const onPressBlockAccount = React.useCallback(async () => {
-      track('ProfileHeader:BlockAccountButtonClicked')
-      store.shell.openModal({
-        name: 'confirm',
-        title: 'Block Account',
-        message:
-          'Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.',
-        onPressConfirm: async () => {
-          try {
-            await view.blockAccount()
-            onRefreshAll()
-            Toast.show('Account blocked')
-          } catch (e: any) {
-            store.log.error('Failed to block account', e)
-            Toast.show(`There was an issue! ${e.toString()}`)
-          }
-        },
-      })
-    }, [track, view, store, onRefreshAll])
+  const onPressUnmuteAccount = React.useCallback(async () => {
+    track('ProfileHeader:UnmuteAccountButtonClicked')
+    try {
+      await view.unmuteAccount()
+      Toast.show('Account unmuted')
+    } catch (e: any) {
+      store.log.error('Failed to unmute account', e)
+      Toast.show(`There was an issue! ${e.toString()}`)
+    }
+  }, [track, view, store])
 
-    const onPressUnblockAccount = React.useCallback(async () => {
-      track('ProfileHeader:UnblockAccountButtonClicked')
-      store.shell.openModal({
-        name: 'confirm',
-        title: 'Unblock Account',
-        message:
-          'The account will be able to interact with you after unblocking.',
-        onPressConfirm: async () => {
-          try {
-            await view.unblockAccount()
-            onRefreshAll()
-            Toast.show('Account unblocked')
-          } catch (e: any) {
-            store.log.error('Failed to unblock account', e)
-            Toast.show(`There was an issue! ${e.toString()}`)
-          }
-        },
-      })
-    }, [track, view, store, onRefreshAll])
+  const onPressBlockAccount = React.useCallback(async () => {
+    track('ProfileHeader:BlockAccountButtonClicked')
+    store.shell.openModal({
+      name: 'confirm',
+      title: 'Block Account',
+      message:
+        'Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.',
+      onPressConfirm: async () => {
+        try {
+          await view.blockAccount()
+          onRefreshAll()
+          Toast.show('Account blocked')
+        } catch (e: any) {
+          store.log.error('Failed to block account', e)
+          Toast.show(`There was an issue! ${e.toString()}`)
+        }
+      },
+    })
+  }, [track, view, store, onRefreshAll])
 
-    const onPressReportAccount = React.useCallback(() => {
-      track('ProfileHeader:ReportAccountButtonClicked')
-      store.shell.openModal({
-        name: 'report',
-        did: view.did,
-      })
-    }, [track, store, view])
+  const onPressUnblockAccount = React.useCallback(async () => {
+    track('ProfileHeader:UnblockAccountButtonClicked')
+    store.shell.openModal({
+      name: 'confirm',
+      title: 'Unblock Account',
+      message:
+        'The account will be able to interact with you after unblocking.',
+      onPressConfirm: async () => {
+        try {
+          await view.unblockAccount()
+          onRefreshAll()
+          Toast.show('Account unblocked')
+        } catch (e: any) {
+          store.log.error('Failed to unblock account', e)
+          Toast.show(`There was an issue! ${e.toString()}`)
+        }
+      },
+    })
+  }, [track, view, store, onRefreshAll])
 
-    const isMe = React.useMemo(
-      () => store.me.did === view.did,
-      [store.me.did, view.did],
-    )
-    const dropdownItems: DropdownItem[] = React.useMemo(() => {
-      let items: DropdownItem[] = [
-        {
-          testID: 'profileHeaderDropdownShareBtn',
-          label: 'Share',
-          onPress: onPressShare,
-          icon: {
-            ios: {
-              name: 'square.and.arrow.up',
-            },
-            android: 'ic_menu_share',
-            web: 'share',
+  const onPressReportAccount = React.useCallback(() => {
+    track('ProfileHeader:ReportAccountButtonClicked')
+    store.shell.openModal({
+      name: 'report',
+      did: view.did,
+    })
+  }, [track, store, view])
+
+  const isMe = React.useMemo(
+    () => store.me.did === view.did,
+    [store.me.did, view.did],
+  )
+  const dropdownItems: DropdownItem[] = React.useMemo(() => {
+    let items: DropdownItem[] = [
+      {
+        testID: 'profileHeaderDropdownShareBtn',
+        label: 'Share',
+        onPress: onPressShare,
+        icon: {
+          ios: {
+            name: 'square.and.arrow.up',
           },
+          android: 'ic_menu_share',
+          web: 'share',
         },
-      ]
-      if (!isMe) {
-        items.push({label: 'separator'})
-        // Only add "Add to Lists" on other user's profiles, doesn't make sense to mute my own self!
-        items.push({
-          testID: 'profileHeaderDropdownListAddRemoveBtn',
-          label: 'Add to Lists',
-          onPress: onPressAddRemoveLists,
-          icon: {
-            ios: {
-              name: 'list.bullet',
-            },
-            android: 'ic_menu_add',
-            web: 'list',
-          },
-        })
-        if (!view.viewer.blocking) {
-          items.push({
-            testID: 'profileHeaderDropdownMuteBtn',
-            label: view.viewer.muted ? 'Unmute Account' : 'Mute Account',
-            onPress: view.viewer.muted
-              ? onPressUnmuteAccount
-              : onPressMuteAccount,
-            icon: {
-              ios: {
-                name: 'speaker.slash',
-              },
-              android: 'ic_lock_silent_mode',
-              web: 'comment-slash',
-            },
-          })
-        }
-        items.push({
-          testID: 'profileHeaderDropdownBlockBtn',
-          label: view.viewer.blocking ? 'Unblock Account' : 'Block Account',
-          onPress: view.viewer.blocking
-            ? onPressUnblockAccount
-            : onPressBlockAccount,
-          icon: {
-            ios: {
-              name: 'person.fill.xmark',
-            },
-            android: 'ic_menu_close_clear_cancel',
-            web: 'user-slash',
+      },
+    ]
+    if (!isMe) {
+      items.push({label: 'separator'})
+      // Only add "Add to Lists" on other user's profiles, doesn't make sense to mute my own self!
+      items.push({
+        testID: 'profileHeaderDropdownListAddRemoveBtn',
+        label: 'Add to Lists',
+        onPress: onPressAddRemoveLists,
+        icon: {
+          ios: {
+            name: 'list.bullet',
           },
-        })
+          android: 'ic_menu_add',
+          web: 'list',
+        },
+      })
+      if (!view.viewer.blocking) {
         items.push({
-          testID: 'profileHeaderDropdownReportBtn',
-          label: 'Report Account',
-          onPress: onPressReportAccount,
+          testID: 'profileHeaderDropdownMuteBtn',
+          label: view.viewer.muted ? 'Unmute Account' : 'Mute Account',
+          onPress: view.viewer.muted
+            ? onPressUnmuteAccount
+            : onPressMuteAccount,
           icon: {
             ios: {
-              name: 'exclamationmark.triangle',
+              name: 'speaker.slash',
             },
-            android: 'ic_menu_report_image',
-            web: 'circle-exclamation',
+            android: 'ic_lock_silent_mode',
+            web: 'comment-slash',
           },
         })
       }
-      return items
-    }, [
-      isMe,
-      view.viewer.muted,
-      view.viewer.blocking,
-      onPressShare,
-      onPressUnmuteAccount,
-      onPressMuteAccount,
-      onPressUnblockAccount,
-      onPressBlockAccount,
-      onPressReportAccount,
-      onPressAddRemoveLists,
-    ])
+      items.push({
+        testID: 'profileHeaderDropdownBlockBtn',
+        label: view.viewer.blocking ? 'Unblock Account' : 'Block Account',
+        onPress: view.viewer.blocking
+          ? onPressUnblockAccount
+          : onPressBlockAccount,
+        icon: {
+          ios: {
+            name: 'person.fill.xmark',
+          },
+          android: 'ic_menu_close_clear_cancel',
+          web: 'user-slash',
+        },
+      })
+      items.push({
+        testID: 'profileHeaderDropdownReportBtn',
+        label: 'Report Account',
+        onPress: onPressReportAccount,
+        icon: {
+          ios: {
+            name: 'exclamationmark.triangle',
+          },
+          android: 'ic_menu_report_image',
+          web: 'circle-exclamation',
+        },
+      })
+    }
+    return items
+  }, [
+    isMe,
+    view.viewer.muted,
+    view.viewer.blocking,
+    onPressShare,
+    onPressUnmuteAccount,
+    onPressMuteAccount,
+    onPressUnblockAccount,
+    onPressBlockAccount,
+    onPressReportAccount,
+    onPressAddRemoveLists,
+  ])
 
-    const blockHide = !isMe && (view.viewer.blocking || view.viewer.blockedBy)
-    const following = formatCount(view.followsCount)
-    const followers = formatCount(view.followersCount)
-    const pluralizedFollowers = pluralize(view.followersCount, 'follower')
+  const blockHide = !isMe && (view.viewer.blocking || view.viewer.blockedBy)
+  const following = formatCount(view.followsCount)
+  const followers = formatCount(view.followersCount)
+  const pluralizedFollowers = pluralize(view.followersCount, 'follower')
 
-    return (
-      <View style={pal.view}>
-        <UserBanner banner={view.banner} moderation={view.moderation.avatar} />
-        <View style={styles.content}>
-          <View style={[styles.buttonsLine]}>
-            {isMe ? (
-              <TouchableOpacity
-                testID="profileHeaderEditProfileButton"
-                onPress={onPressEditProfile}
-                style={[styles.btn, styles.mainBtn, pal.btn]}
-                accessibilityRole="button"
-                accessibilityLabel="Edit profile"
-                accessibilityHint="Opens editor for profile display name, avatar, background image, and description">
-                <Text type="button" style={pal.text}>
-                  Edit Profile
-                </Text>
-              </TouchableOpacity>
-            ) : view.viewer.blocking ? (
-              <TouchableOpacity
-                testID="unblockBtn"
-                onPress={onPressUnblockAccount}
-                style={[styles.btn, styles.mainBtn, pal.btn]}
-                accessibilityRole="button"
-                accessibilityLabel="Unblock"
-                accessibilityHint="">
-                <Text type="button" style={[pal.text, s.bold]}>
-                  Unblock
-                </Text>
-              </TouchableOpacity>
-            ) : !view.viewer.blockedBy ? (
-              <>
-                {store.me.follows.getFollowState(view.did) ===
-                FollowState.Following ? (
-                  <TouchableOpacity
-                    testID="unfollowBtn"
-                    onPress={onPressToggleFollow}
-                    style={[styles.btn, styles.mainBtn, pal.btn]}
-                    accessibilityRole="button"
-                    accessibilityLabel={`Unfollow ${view.handle}`}
-                    accessibilityHint={`Hides posts from ${view.handle} in your feed`}>
-                    <FontAwesomeIcon
-                      icon="check"
-                      style={[pal.text, s.mr5]}
-                      size={14}
-                    />
-                    <Text type="button" style={pal.text}>
-                      Following
-                    </Text>
-                  </TouchableOpacity>
-                ) : (
-                  <TouchableOpacity
-                    testID="followBtn"
-                    onPress={onPressToggleFollow}
-                    style={[styles.btn, styles.mainBtn, palInverted.view]}
-                    accessibilityRole="button"
-                    accessibilityLabel={`Follow ${view.handle}`}
-                    accessibilityHint={`Shows posts from ${view.handle} in your feed`}>
-                    <FontAwesomeIcon
-                      icon="plus"
-                      style={[palInverted.text, s.mr5]}
-                    />
-                    <Text type="button" style={[palInverted.text, s.bold]}>
-                      Follow
-                    </Text>
-                  </TouchableOpacity>
-                )}
-              </>
-            ) : null}
-            {dropdownItems?.length ? (
-              <NativeDropdown
-                testID="profileHeaderDropdownBtn"
-                items={dropdownItems}>
-                <View style={[styles.btn, styles.secondaryBtn, pal.btn]}>
-                  <FontAwesomeIcon
-                    icon="ellipsis"
-                    size={20}
-                    style={[pal.text]}
-                  />
-                </View>
-              </NativeDropdown>
-            ) : undefined}
-          </View>
-          <View>
-            <Text
-              testID="profileHeaderDisplayName"
-              type="title-2xl"
-              style={[pal.text, styles.title]}>
-              {sanitizeDisplayName(
-                view.displayName || sanitizeHandle(view.handle),
-                view.moderation.profile,
-              )}
-            </Text>
-          </View>
-          <View style={styles.handleLine}>
-            {view.viewer.followedBy && !blockHide ? (
-              <View style={[styles.pill, pal.btn, s.mr5]}>
-                <Text type="xs" style={[pal.text]}>
-                  Follows you
-                </Text>
-              </View>
-            ) : undefined}
-            <ThemedText
-              type={invalidHandle ? 'xs' : 'md'}
-              fg={invalidHandle ? 'error' : 'light'}
-              border={invalidHandle ? 'error' : undefined}
-              style={[
-                invalidHandle ? styles.invalidHandle : undefined,
-                styles.handle,
-              ]}>
-              {invalidHandle ? '⚠Invalid Handle' : `@${view.handle}`}
-            </ThemedText>
-          </View>
-          {!blockHide && (
+  return (
+    <View style={pal.view}>
+      <UserBanner banner={view.banner} moderation={view.moderation.avatar} />
+      <View style={styles.content}>
+        <View style={[styles.buttonsLine]}>
+          {isMe ? (
+            <TouchableOpacity
+              testID="profileHeaderEditProfileButton"
+              onPress={onPressEditProfile}
+              style={[styles.btn, styles.mainBtn, pal.btn]}
+              accessibilityRole="button"
+              accessibilityLabel="Edit profile"
+              accessibilityHint="Opens editor for profile display name, avatar, background image, and description">
+              <Text type="button" style={pal.text}>
+                Edit Profile
+              </Text>
+            </TouchableOpacity>
+          ) : view.viewer.blocking ? (
+            <TouchableOpacity
+              testID="unblockBtn"
+              onPress={onPressUnblockAccount}
+              style={[styles.btn, styles.mainBtn, pal.btn]}
+              accessibilityRole="button"
+              accessibilityLabel="Unblock"
+              accessibilityHint="">
+              <Text type="button" style={[pal.text, s.bold]}>
+                Unblock
+              </Text>
+            </TouchableOpacity>
+          ) : !view.viewer.blockedBy ? (
             <>
-              <View style={styles.metricsLine}>
+              {store.me.follows.getFollowState(view.did) ===
+              FollowState.Following ? (
                 <TouchableOpacity
-                  testID="profileHeaderFollowersButton"
-                  style={[s.flexRow, s.mr10]}
-                  onPress={onPressFollowers}
+                  testID="unfollowBtn"
+                  onPress={onPressToggleFollow}
+                  style={[styles.btn, styles.mainBtn, pal.btn]}
                   accessibilityRole="button"
-                  accessibilityLabel={`${followers} ${pluralizedFollowers}`}
-                  accessibilityHint={'Opens followers list'}>
-                  <Text type="md" style={[s.bold, pal.text]}>
-                    {followers}{' '}
-                  </Text>
-                  <Text type="md" style={[pal.textLight]}>
-                    {pluralizedFollowers}
+                  accessibilityLabel={`Unfollow ${view.handle}`}
+                  accessibilityHint={`Hides posts from ${view.handle} in your feed`}>
+                  <FontAwesomeIcon
+                    icon="check"
+                    style={[pal.text, s.mr5]}
+                    size={14}
+                  />
+                  <Text type="button" style={pal.text}>
+                    Following
                   </Text>
                 </TouchableOpacity>
+              ) : (
                 <TouchableOpacity
-                  testID="profileHeaderFollowsButton"
-                  style={[s.flexRow, s.mr10]}
-                  onPress={onPressFollows}
+                  testID="followBtn"
+                  onPress={onPressToggleFollow}
+                  style={[styles.btn, styles.mainBtn, palInverted.view]}
                   accessibilityRole="button"
-                  accessibilityLabel={`${following} following`}
-                  accessibilityHint={'Opens following list'}>
-                  <Text type="md" style={[s.bold, pal.text]}>
-                    {following}{' '}
-                  </Text>
-                  <Text type="md" style={[pal.textLight]}>
-                    following
+                  accessibilityLabel={`Follow ${view.handle}`}
+                  accessibilityHint={`Shows posts from ${view.handle} in your feed`}>
+                  <FontAwesomeIcon
+                    icon="plus"
+                    style={[palInverted.text, s.mr5]}
+                  />
+                  <Text type="button" style={[palInverted.text, s.bold]}>
+                    Follow
                   </Text>
                 </TouchableOpacity>
-                <Text type="md" style={[s.bold, pal.text]}>
-                  {formatCount(view.postsCount)}{' '}
-                  <Text type="md" style={[pal.textLight]}>
-                    {pluralize(view.postsCount, 'post')}
-                  </Text>
-                </Text>
-              </View>
-              {view.description &&
-              view.descriptionRichText &&
-              !view.moderation.profile.blur ? (
-                <RichText
-                  testID="profileHeaderDescription"
-                  style={[styles.description, pal.text]}
-                  numberOfLines={15}
-                  richText={view.descriptionRichText}
-                />
-              ) : undefined}
+              )}
             </>
-          )}
-          <ProfileHeaderAlerts moderation={view.moderation} />
+          ) : null}
+          {dropdownItems?.length ? (
+            <NativeDropdown
+              testID="profileHeaderDropdownBtn"
+              items={dropdownItems}>
+              <View style={[styles.btn, styles.secondaryBtn, pal.btn]}>
+                <FontAwesomeIcon icon="ellipsis" size={20} style={[pal.text]} />
+              </View>
+            </NativeDropdown>
+          ) : undefined}
+        </View>
+        <View>
+          <Text
+            testID="profileHeaderDisplayName"
+            type="title-2xl"
+            style={[pal.text, styles.title]}>
+            {sanitizeDisplayName(
+              view.displayName || sanitizeHandle(view.handle),
+              view.moderation.profile,
+            )}
+          </Text>
+        </View>
+        <View style={styles.handleLine}>
+          {view.viewer.followedBy && !blockHide ? (
+            <View style={[styles.pill, pal.btn, s.mr5]}>
+              <Text type="xs" style={[pal.text]}>
+                Follows you
+              </Text>
+            </View>
+          ) : undefined}
+          <ThemedText
+            type={invalidHandle ? 'xs' : 'md'}
+            fg={invalidHandle ? 'error' : 'light'}
+            border={invalidHandle ? 'error' : undefined}
+            style={[
+              invalidHandle ? styles.invalidHandle : undefined,
+              styles.handle,
+            ]}>
+            {invalidHandle ? '⚠Invalid Handle' : `@${view.handle}`}
+          </ThemedText>
         </View>
-        {!isDesktop && !hideBackButton && (
-          <TouchableWithoutFeedback
-            onPress={onPressBack}
-            hitSlop={BACK_HITSLOP}
-            accessibilityRole="button"
-            accessibilityLabel="Back"
-            accessibilityHint="">
-            <View style={styles.backBtnWrapper}>
-              <BlurView style={styles.backBtn} blurType="dark">
-                <FontAwesomeIcon size={18} icon="angle-left" style={s.white} />
-              </BlurView>
+        {!blockHide && (
+          <>
+            <View style={styles.metricsLine}>
+              <TouchableOpacity
+                testID="profileHeaderFollowersButton"
+                style={[s.flexRow, s.mr10]}
+                onPress={onPressFollowers}
+                accessibilityRole="button"
+                accessibilityLabel={`${followers} ${pluralizedFollowers}`}
+                accessibilityHint={'Opens followers list'}>
+                <Text type="md" style={[s.bold, pal.text]}>
+                  {followers}{' '}
+                </Text>
+                <Text type="md" style={[pal.textLight]}>
+                  {pluralizedFollowers}
+                </Text>
+              </TouchableOpacity>
+              <TouchableOpacity
+                testID="profileHeaderFollowsButton"
+                style={[s.flexRow, s.mr10]}
+                onPress={onPressFollows}
+                accessibilityRole="button"
+                accessibilityLabel={`${following} following`}
+                accessibilityHint={'Opens following list'}>
+                <Text type="md" style={[s.bold, pal.text]}>
+                  {following}{' '}
+                </Text>
+                <Text type="md" style={[pal.textLight]}>
+                  following
+                </Text>
+              </TouchableOpacity>
+              <Text type="md" style={[s.bold, pal.text]}>
+                {formatCount(view.postsCount)}{' '}
+                <Text type="md" style={[pal.textLight]}>
+                  {pluralize(view.postsCount, 'post')}
+                </Text>
+              </Text>
             </View>
-          </TouchableWithoutFeedback>
+            {view.description &&
+            view.descriptionRichText &&
+            !view.moderation.profile.blur ? (
+              <RichText
+                testID="profileHeaderDescription"
+                style={[styles.description, pal.text]}
+                numberOfLines={15}
+                richText={view.descriptionRichText}
+              />
+            ) : undefined}
+          </>
         )}
+        <ProfileHeaderAlerts moderation={view.moderation} />
+      </View>
+      {!isDesktop && !hideBackButton && (
         <TouchableWithoutFeedback
-          testID="profileHeaderAviButton"
-          onPress={onPressAvi}
-          accessibilityRole="image"
-          accessibilityLabel={`View ${view.handle}'s avatar`}
+          onPress={onPressBack}
+          hitSlop={BACK_HITSLOP}
+          accessibilityRole="button"
+          accessibilityLabel="Back"
           accessibilityHint="">
-          <View
-            style={[
-              pal.view,
-              {borderColor: pal.colors.background},
-              styles.avi,
-            ]}>
-            <UserAvatar
-              size={80}
-              avatar={view.avatar}
-              moderation={view.moderation.avatar}
-            />
+          <View style={styles.backBtnWrapper}>
+            <BlurView style={styles.backBtn} blurType="dark">
+              <FontAwesomeIcon size={18} icon="angle-left" style={s.white} />
+            </BlurView>
           </View>
         </TouchableWithoutFeedback>
-      </View>
-    )
-  },
-)
+      )}
+      <TouchableWithoutFeedback
+        testID="profileHeaderAviButton"
+        onPress={onPressAvi}
+        accessibilityRole="image"
+        accessibilityLabel={`View ${view.handle}'s avatar`}
+        accessibilityHint="">
+        <View
+          style={[pal.view, {borderColor: pal.colors.background}, styles.avi]}>
+          <UserAvatar
+            size={80}
+            avatar={view.avatar}
+            moderation={view.moderation.avatar}
+          />
+        </View>
+      </TouchableWithoutFeedback>
+    </View>
+  )
+})
 
 const styles = StyleSheet.create({
   banner: {