about summary refs log tree commit diff
path: root/src/view/com/profile/ProfileHeader.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/com/profile/ProfileHeader.tsx')
-rw-r--r--src/view/com/profile/ProfileHeader.tsx505
1 files changed, 283 insertions, 222 deletions
diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx
index 1a1d38e4b..8058551c2 100644
--- a/src/view/com/profile/ProfileHeader.tsx
+++ b/src/view/com/profile/ProfileHeader.tsx
@@ -1,5 +1,4 @@
-import React from 'react'
-import {observer} from 'mobx-react-lite'
+import React, {memo} from 'react'
 import {
   StyleSheet,
   TouchableOpacity,
@@ -8,15 +7,17 @@ import {
 } from 'react-native'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {useNavigation} from '@react-navigation/native'
+import {useQueryClient} from '@tanstack/react-query'
+import {
+  AppBskyActorDefs,
+  ProfileModeration,
+  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, isWeb} from 'platform/detection'
 import {BlurView} from '../util/BlurView'
-import {ProfileModel} from 'state/models/content/profile'
-import {useStores} from 'state/index'
-import {ProfileImageLightbox} from 'state/models/ui/shell'
-import {pluralize} from 'lib/strings/helpers'
-import {toShareUrl} from 'lib/strings/url-helpers'
-import {sanitizeDisplayName} from 'lib/strings/display-names'
-import {sanitizeHandle} from 'lib/strings/handles'
-import {s, colors} from 'lib/styles'
 import * as Toast from '../util/Toast'
 import {LoadingPlaceholder} from '../util/LoadingPlaceholder'
 import {Text} from '../util/text/Text'
@@ -25,32 +26,45 @@ import {RichText} from '../util/text/RichText'
 import {UserAvatar} from '../util/UserAvatar'
 import {UserBanner} from '../util/UserBanner'
 import {ProfileHeaderAlerts} from '../util/moderation/ProfileHeaderAlerts'
+import {formatCount} from '../util/numeric/format'
+import {NativeDropdown, DropdownItem} from '../util/forms/NativeDropdown'
+import {Link} from '../util/Link'
+import {ProfileHeaderSuggestedFollows} from './ProfileHeaderSuggestedFollows'
+import {useModalControls} from '#/state/modals'
+import {useLightboxControls, ProfileImageLightbox} from '#/state/lightbox'
+import {
+  RQKEY as profileQueryKey,
+  useProfileMuteMutationQueue,
+  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 {NavigationProp} from 'lib/routes/types'
-import {isNative} from 'platform/detection'
-import {FollowState} from 'state/models/cache/my-follows'
-import {shareUrl} from 'lib/sharing'
-import {formatCount} from '../util/numeric/format'
-import {NativeDropdown, DropdownItem} from '../util/forms/NativeDropdown'
 import {BACK_HITSLOP} from 'lib/constants'
 import {isInvalidHandle} from 'lib/strings/handles'
 import {makeProfileLink} from 'lib/routes/links'
-import {Link} from '../util/Link'
-import {ProfileHeaderSuggestedFollows} from './ProfileHeaderSuggestedFollows'
+import {pluralize} from 'lib/strings/helpers'
+import {toShareUrl} from 'lib/strings/url-helpers'
+import {sanitizeDisplayName} from 'lib/strings/display-names'
+import {sanitizeHandle} from 'lib/strings/handles'
+import {shareUrl} from 'lib/sharing'
+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'
 
 interface Props {
-  view: ProfileModel
-  onRefreshAll: () => void
+  profile: Shadow<AppBskyActorDefs.ProfileViewDetailed> | null
+  moderation: ProfileModeration | null
   hideBackButton?: boolean
   isProfilePreview?: boolean
 }
 
-export const ProfileHeader = observer(function ProfileHeaderImpl({
-  view,
-  onRefreshAll,
+export function ProfileHeader({
+  profile,
+  moderation,
   hideBackButton = false,
   isProfilePreview,
 }: Props) {
@@ -58,7 +72,7 @@ export const ProfileHeader = observer(function ProfileHeaderImpl({
 
   // loading
   // =
-  if (!view || !view.hasLoaded) {
+  if (!profile || !moderation) {
     return (
       <View style={pal.view}>
         <LoadingPlaceholder width="100%" height={153} />
@@ -70,54 +84,65 @@ export const ProfileHeader = observer(function ProfileHeaderImpl({
           <View style={[styles.buttonsLine]}>
             <LoadingPlaceholder width={167} 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>
       </View>
     )
   }
 
-  // error
-  // =
-  if (view.hasError) {
-    return (
-      <View testID="profileHeaderHasError">
-        <Text>{view.error}</Text>
-      </View>
-    )
-  }
-
   // loaded
   // =
   return (
     <ProfileHeaderLoaded
-      view={view}
-      onRefreshAll={onRefreshAll}
+      profile={profile}
+      moderation={moderation}
       hideBackButton={hideBackButton}
       isProfilePreview={isProfilePreview}
     />
   )
-})
+}
+
+interface LoadedProps {
+  profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>
+  moderation: ProfileModeration
+  hideBackButton?: boolean
+  isProfilePreview?: boolean
+}
 
-const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
-  view,
-  onRefreshAll,
+let ProfileHeaderLoaded = ({
+  profile,
+  moderation,
   hideBackButton = false,
   isProfilePreview,
-}: Props) {
+}: LoadedProps): React.ReactNode => {
   const pal = usePalette('default')
   const palInverted = usePalette('inverted')
-  const store = useStores()
+  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(view.handle)
+  const invalidHandle = isInvalidHandle(profile.handle)
   const {isDesktop} = useWebMediaQueries()
   const [showSuggestedFollows, setShowSuggestedFollows] = React.useState(false)
+  const descriptionRT = React.useMemo(
+    () =>
+      profile.description
+        ? new RichTextAPI({text: profile.description})
+        : undefined,
+    [profile],
+  )
+  const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(profile)
+  const [queueMute, queueUnmute] = useProfileMuteMutationQueue(profile)
+  const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile)
+  const queryClient = useQueryClient()
+
+  const invalidateProfileQuery = React.useCallback(() => {
+    queryClient.invalidateQueries({
+      queryKey: profileQueryKey(profile.did),
+    })
+  }, [queryClient, profile.did])
 
   const onPressBack = React.useCallback(() => {
     if (navigation.canGoBack()) {
@@ -129,144 +154,162 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
 
   const onPressAvi = React.useCallback(() => {
     if (
-      view.avatar &&
-      !(view.moderation.avatar.blur && view.moderation.avatar.noOverride)
+      profile.avatar &&
+      !(moderation.avatar.blur && moderation.avatar.noOverride)
     ) {
-      store.shell.openLightbox(new ProfileImageLightbox(view))
+      openLightbox(new ProfileImageLightbox(profile))
     }
-  }, [store, view])
+  }, [openLightbox, profile, moderation])
 
-  const onPressToggleFollow = React.useCallback(() => {
-    view?.toggleFollowing().then(
-      () => {
-        setShowSuggestedFollows(Boolean(view.viewer.following))
+  const onPressFollow = () => {
+    requireAuth(async () => {
+      try {
+        track('ProfileHeader:FollowButtonClicked')
+        await queueFollow()
         Toast.show(
-          `${
-            view.viewer.following ? 'Following' : 'No longer following'
-          } ${sanitizeDisplayName(view.displayName || view.handle)}`,
+          `Following ${sanitizeDisplayName(
+            profile.displayName || profile.handle,
+          )}`,
         )
-        track(
-          view.viewer.following
-            ? 'ProfileHeader:FollowButtonClicked'
-            : 'ProfileHeader:UnfollowButtonClicked',
+      } catch (e: any) {
+        if (e?.name !== 'AbortError') {
+          logger.error('Failed to follow', {error: String(e)})
+          Toast.show(`There was an issue! ${e.toString()}`)
+        }
+      }
+    })
+  }
+
+  const onPressUnfollow = () => {
+    requireAuth(async () => {
+      try {
+        track('ProfileHeader:UnfollowButtonClicked')
+        await queueUnfollow()
+        Toast.show(
+          `No longer following ${sanitizeDisplayName(
+            profile.displayName || profile.handle,
+          )}`,
         )
-      },
-      err => logger.error('Failed to toggle follow', {error: err}),
-    )
-  }, [track, view, setShowSuggestedFollows])
+      } catch (e: any) {
+        if (e?.name !== 'AbortError') {
+          logger.error('Failed to unfollow', {error: String(e)})
+          Toast.show(`There was an issue! ${e.toString()}`)
+        }
+      }
+    })
+  }
 
   const onPressEditProfile = React.useCallback(() => {
     track('ProfileHeader:EditProfileButtonClicked')
-    store.shell.openModal({
+    openModal({
       name: 'edit-profile',
-      profileView: view,
-      onUpdate: onRefreshAll,
+      profile,
     })
-  }, [track, store, view, onRefreshAll])
-
-  const trackPress = React.useCallback(
-    (f: 'Followers' | 'Follows') => {
-      track(`ProfileHeader:${f}ButtonClicked`, {
-        handle: view.handle,
-      })
-    },
-    [track, view],
-  )
+  }, [track, openModal, profile])
 
   const onPressShare = React.useCallback(() => {
     track('ProfileHeader:ShareButtonClicked')
-    const url = toShareUrl(makeProfileLink(view))
-    shareUrl(url)
-  }, [track, view])
+    shareUrl(toShareUrl(makeProfileLink(profile)))
+  }, [track, profile])
 
   const onPressAddRemoveLists = React.useCallback(() => {
     track('ProfileHeader:AddToListsButtonClicked')
-    store.shell.openModal({
+    openModal({
       name: 'user-add-remove-lists',
-      subject: view.did,
-      displayName: view.displayName || view.handle,
+      subject: profile.did,
+      displayName: profile.displayName || profile.handle,
+      onAdd: invalidateProfileQuery,
+      onRemove: invalidateProfileQuery,
     })
-  }, [track, view, store])
+  }, [track, profile, openModal, invalidateProfileQuery])
 
   const onPressMuteAccount = React.useCallback(async () => {
     track('ProfileHeader:MuteAccountButtonClicked')
     try {
-      await view.muteAccount()
+      await queueMute()
       Toast.show('Account muted')
     } catch (e: any) {
-      logger.error('Failed to mute account', {error: e})
-      Toast.show(`There was an issue! ${e.toString()}`)
+      if (e?.name !== 'AbortError') {
+        logger.error('Failed to mute account', {error: e})
+        Toast.show(`There was an issue! ${e.toString()}`)
+      }
     }
-  }, [track, view])
+  }, [track, queueMute])
 
   const onPressUnmuteAccount = React.useCallback(async () => {
     track('ProfileHeader:UnmuteAccountButtonClicked')
     try {
-      await view.unmuteAccount()
+      await queueUnmute()
       Toast.show('Account unmuted')
     } catch (e: any) {
-      logger.error('Failed to unmute account', {error: e})
-      Toast.show(`There was an issue! ${e.toString()}`)
+      if (e?.name !== 'AbortError') {
+        logger.error('Failed to unmute account', {error: e})
+        Toast.show(`There was an issue! ${e.toString()}`)
+      }
     }
-  }, [track, view])
+  }, [track, queueUnmute])
 
   const onPressBlockAccount = React.useCallback(async () => {
     track('ProfileHeader:BlockAccountButtonClicked')
-    store.shell.openModal({
+    openModal({
       name: 'confirm',
-      title: 'Block Account',
-      message:
-        'Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.',
+      title: _(msg`Block Account`),
+      message: _(
+        msg`Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`,
+      ),
       onPressConfirm: async () => {
         try {
-          await view.blockAccount()
-          onRefreshAll()
+          await queueBlock()
           Toast.show('Account blocked')
         } catch (e: any) {
-          logger.error('Failed to block account', {error: e})
-          Toast.show(`There was an issue! ${e.toString()}`)
+          if (e?.name !== 'AbortError') {
+            logger.error('Failed to block account', {error: e})
+            Toast.show(`There was an issue! ${e.toString()}`)
+          }
         }
       },
     })
-  }, [track, view, store, onRefreshAll])
+  }, [track, queueBlock, openModal, _])
 
   const onPressUnblockAccount = React.useCallback(async () => {
     track('ProfileHeader:UnblockAccountButtonClicked')
-    store.shell.openModal({
+    openModal({
       name: 'confirm',
-      title: 'Unblock Account',
-      message:
-        'The account will be able to interact with you after unblocking.',
+      title: _(msg`Unblock Account`),
+      message: _(
+        msg`The account will be able to interact with you after unblocking.`,
+      ),
       onPressConfirm: async () => {
         try {
-          await view.unblockAccount()
-          onRefreshAll()
+          await queueUnblock()
           Toast.show('Account unblocked')
         } catch (e: any) {
-          logger.error('Failed to unblock account', {error: e})
-          Toast.show(`There was an issue! ${e.toString()}`)
+          if (e?.name !== 'AbortError') {
+            logger.error('Failed to unblock account', {error: e})
+            Toast.show(`There was an issue! ${e.toString()}`)
+          }
         }
       },
     })
-  }, [track, view, store, onRefreshAll])
+  }, [track, queueUnblock, openModal, _])
 
   const onPressReportAccount = React.useCallback(() => {
     track('ProfileHeader:ReportAccountButtonClicked')
-    store.shell.openModal({
+    openModal({
       name: 'report',
-      did: view.did,
+      did: profile.did,
     })
-  }, [track, store, view])
+  }, [track, openModal, profile])
 
   const isMe = React.useMemo(
-    () => store.me.did === view.did,
-    [store.me.did, view.did],
+    () => currentAccount?.did === profile.did,
+    [currentAccount, profile],
   )
   const dropdownItems: DropdownItem[] = React.useMemo(() => {
     let items: DropdownItem[] = [
       {
         testID: 'profileHeaderDropdownShareBtn',
-        label: 'Share',
+        label: isWeb ? _(msg`Copy link to profile`) : _(msg`Share`),
         onPress: onPressShare,
         icon: {
           ios: {
@@ -277,71 +320,81 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
         },
       },
     ]
-    items.push({label: 'separator'})
-    items.push({
-      testID: 'profileHeaderDropdownListAddRemoveBtn',
-      label: 'Add to Lists',
-      onPress: onPressAddRemoveLists,
-      icon: {
-        ios: {
-          name: 'list.bullet',
+    if (hasSession) {
+      items.push({label: 'separator'})
+      items.push({
+        testID: 'profileHeaderDropdownListAddRemoveBtn',
+        label: _(msg`Add to Lists`),
+        onPress: onPressAddRemoveLists,
+        icon: {
+          ios: {
+            name: 'list.bullet',
+          },
+          android: 'ic_menu_add',
+          web: 'list',
         },
-        android: 'ic_menu_add',
-        web: 'list',
-      },
-    })
-    if (!isMe) {
-      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',
+      })
+      if (!isMe) {
+        if (!profile.viewer?.blocking) {
+          if (!profile.viewer?.mutedByList) {
+            items.push({
+              testID: 'profileHeaderDropdownMuteBtn',
+              label: profile.viewer?.muted
+                ? _(msg`Unmute Account`)
+                : _(msg`Mute Account`),
+              onPress: profile.viewer?.muted
+                ? onPressUnmuteAccount
+                : onPressMuteAccount,
+              icon: {
+                ios: {
+                  name: 'speaker.slash',
+                },
+                android: 'ic_lock_silent_mode',
+                web: 'comment-slash',
+              },
+            })
+          }
+        }
+        if (!profile.viewer?.blockingByList) {
+          items.push({
+            testID: 'profileHeaderDropdownBlockBtn',
+            label: profile.viewer?.blocking
+              ? _(msg`Unblock Account`)
+              : _(msg`Block Account`),
+            onPress: profile.viewer?.blocking
+              ? onPressUnblockAccount
+              : onPressBlockAccount,
+            icon: {
+              ios: {
+                name: 'person.fill.xmark',
+              },
+              android: 'ic_menu_close_clear_cancel',
+              web: 'user-slash',
             },
-            android: 'ic_lock_silent_mode',
-            web: 'comment-slash',
-          },
-        })
-      }
-      if (!view.viewer.blockingByList) {
+          })
+        }
         items.push({
-          testID: 'profileHeaderDropdownBlockBtn',
-          label: view.viewer.blocking ? 'Unblock Account' : 'Block Account',
-          onPress: view.viewer.blocking
-            ? onPressUnblockAccount
-            : onPressBlockAccount,
+          testID: 'profileHeaderDropdownReportBtn',
+          label: _(msg`Report Account`),
+          onPress: onPressReportAccount,
           icon: {
             ios: {
-              name: 'person.fill.xmark',
+              name: 'exclamationmark.triangle',
             },
-            android: 'ic_menu_close_clear_cancel',
-            web: 'user-slash',
+            android: 'ic_menu_report_image',
+            web: 'circle-exclamation',
           },
         })
       }
-      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,
-    view.viewer.blockingByList,
+    hasSession,
+    profile.viewer?.muted,
+    profile.viewer?.mutedByList,
+    profile.viewer?.blocking,
+    profile.viewer?.blockingByList,
     onPressShare,
     onPressUnmuteAccount,
     onPressMuteAccount,
@@ -349,16 +402,18 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
     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 && (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}>
-      <UserBanner banner={view.banner} moderation={view.moderation.avatar} />
+      <UserBanner banner={profile.banner} moderation={moderation.avatar} />
       <View style={styles.content}>
         <View style={[styles.buttonsLine]}>
           {isMe ? (
@@ -367,29 +422,29 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
               onPress={onPressEditProfile}
               style={[styles.btn, styles.mainBtn, pal.btn]}
               accessibilityRole="button"
-              accessibilityLabel="Edit profile"
+              accessibilityLabel={_(msg`Edit profile`)}
               accessibilityHint="Opens editor for profile display name, avatar, background image, and description">
               <Text type="button" style={pal.text}>
-                Edit Profile
+                <Trans>Edit Profile</Trans>
               </Text>
             </TouchableOpacity>
-          ) : view.viewer.blocking ? (
-            view.viewer.blockingByList ? null : (
+          ) : profile.viewer?.blocking ? (
+            profile.viewer?.blockingByList ? null : (
               <TouchableOpacity
                 testID="unblockBtn"
                 onPress={onPressUnblockAccount}
                 style={[styles.btn, styles.mainBtn, pal.btn]}
                 accessibilityRole="button"
-                accessibilityLabel="Unblock"
+                accessibilityLabel={_(msg`Unblock`)}
                 accessibilityHint="">
                 <Text type="button" style={[pal.text, s.bold]}>
-                  Unblock
+                  <Trans>Unblock</Trans>
                 </Text>
               </TouchableOpacity>
             )
-          ) : !view.viewer.blockedBy ? (
+          ) : !profile.viewer?.blockedBy ? (
             <>
-              {!isProfilePreview && (
+              {!isProfilePreview && hasSession && (
                 <TouchableOpacity
                   testID="suggestedFollowsBtn"
                   onPress={() => setShowSuggestedFollows(!showSuggestedFollows)}
@@ -405,7 +460,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
                     },
                   ]}
                   accessibilityRole="button"
-                  accessibilityLabel={`Show follows similar to ${view.handle}`}
+                  accessibilityLabel={`Show follows similar to ${profile.handle}`}
                   accessibilityHint={`Shows a list of users similar to this user.`}>
                   <FontAwesomeIcon
                     icon="user-plus"
@@ -413,7 +468,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
                       pal.text,
                       {
                         color: showSuggestedFollows
-                          ? colors.white
+                          ? pal.textInverted.color
                           : pal.text.color,
                       },
                     ]}
@@ -422,38 +477,37 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
                 </TouchableOpacity>
               )}
 
-              {store.me.follows.getFollowState(view.did) ===
-              FollowState.Following ? (
+              {profile.viewer?.following ? (
                 <TouchableOpacity
                   testID="unfollowBtn"
-                  onPress={onPressToggleFollow}
+                  onPress={onPressUnfollow}
                   style={[styles.btn, styles.mainBtn, pal.btn]}
                   accessibilityRole="button"
-                  accessibilityLabel={`Unfollow ${view.handle}`}
-                  accessibilityHint={`Hides posts from ${view.handle} in your feed`}>
+                  accessibilityLabel={`Unfollow ${profile.handle}`}
+                  accessibilityHint={`Hides posts from ${profile.handle} in your feed`}>
                   <FontAwesomeIcon
                     icon="check"
                     style={[pal.text, s.mr5]}
                     size={14}
                   />
                   <Text type="button" style={pal.text}>
-                    Following
+                    <Trans>Following</Trans>
                   </Text>
                 </TouchableOpacity>
               ) : (
                 <TouchableOpacity
                   testID="followBtn"
-                  onPress={onPressToggleFollow}
+                  onPress={onPressFollow}
                   style={[styles.btn, styles.mainBtn, palInverted.view]}
                   accessibilityRole="button"
-                  accessibilityLabel={`Follow ${view.handle}`}
-                  accessibilityHint={`Shows posts from ${view.handle} in your feed`}>
+                  accessibilityLabel={`Follow ${profile.handle}`}
+                  accessibilityHint={`Shows posts from ${profile.handle} in your feed`}>
                   <FontAwesomeIcon
                     icon="plus"
                     style={[palInverted.text, s.mr5]}
                   />
                   <Text type="button" style={[palInverted.text, s.bold]}>
-                    Follow
+                    <Trans>Follow</Trans>
                   </Text>
                 </TouchableOpacity>
               )}
@@ -463,7 +517,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
             <NativeDropdown
               testID="profileHeaderDropdownBtn"
               items={dropdownItems}
-              accessibilityLabel="More options"
+              accessibilityLabel={_(msg`More options`)}
               accessibilityHint="">
               <View style={[styles.btn, styles.secondaryBtn, pal.btn]}>
                 <FontAwesomeIcon icon="ellipsis" size={20} style={[pal.text]} />
@@ -477,16 +531,16 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
             type="title-2xl"
             style={[pal.text, styles.title]}>
             {sanitizeDisplayName(
-              view.displayName || sanitizeHandle(view.handle),
-              view.moderation.profile,
+              profile.displayName || sanitizeHandle(profile.handle),
+              moderation.profile,
             )}
           </Text>
         </View>
         <View style={styles.handleLine}>
-          {view.viewer.followedBy && !blockHide ? (
+          {profile.viewer?.followedBy && !blockHide ? (
             <View style={[styles.pill, pal.btn, s.mr5]}>
               <Text type="xs" style={[pal.text]}>
-                Follows you
+                <Trans>Follows you</Trans>
               </Text>
             </View>
           ) : undefined}
@@ -498,7 +552,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
               invalidHandle ? styles.invalidHandle : undefined,
               styles.handle,
             ]}>
-            {invalidHandle ? '⚠Invalid Handle' : `@${view.handle}`}
+            {invalidHandle ? '⚠Invalid Handle' : `@${profile.handle}`}
           </ThemedText>
         </View>
         {!blockHide && (
@@ -507,8 +561,12 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
               <Link
                 testID="profileHeaderFollowersButton"
                 style={[s.flexRow, s.mr10]}
-                href={makeProfileLink(view, 'followers')}
-                onPressOut={() => trackPress('Followers')}
+                href={makeProfileLink(profile, 'followers')}
+                onPressOut={() =>
+                  track(`ProfileHeader:FollowersButtonClicked`, {
+                    handle: profile.handle,
+                  })
+                }
                 asAnchor
                 accessibilityLabel={`${followers} ${pluralizedFollowers}`}
                 accessibilityHint={'Opens followers list'}>
@@ -522,8 +580,12 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
               <Link
                 testID="profileHeaderFollowsButton"
                 style={[s.flexRow, s.mr10]}
-                href={makeProfileLink(view, 'follows')}
-                onPressOut={() => trackPress('Follows')}
+                href={makeProfileLink(profile, 'follows')}
+                onPressOut={() =>
+                  track(`ProfileHeader:FollowsButtonClicked`, {
+                    handle: profile.handle,
+                  })
+                }
                 asAnchor
                 accessibilityLabel={`${following} following`}
                 accessibilityHint={'Opens following list'}>
@@ -531,34 +593,32 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
                   {following}{' '}
                 </Text>
                 <Text type="md" style={[pal.textLight]}>
-                  following
+                  <Trans>following</Trans>
                 </Text>
               </Link>
               <Text type="md" style={[s.bold, pal.text]}>
-                {formatCount(view.postsCount)}{' '}
+                {formatCount(profile.postsCount || 0)}{' '}
                 <Text type="md" style={[pal.textLight]}>
-                  {pluralize(view.postsCount, 'post')}
+                  {pluralize(profile.postsCount || 0, 'post')}
                 </Text>
               </Text>
             </View>
-            {view.description &&
-            view.descriptionRichText &&
-            !view.moderation.profile.blur ? (
+            {descriptionRT && !moderation.profile.blur ? (
               <RichText
                 testID="profileHeaderDescription"
                 style={[styles.description, pal.text]}
                 numberOfLines={15}
-                richText={view.descriptionRichText}
+                richText={descriptionRT}
               />
             ) : undefined}
           </>
         )}
-        <ProfileHeaderAlerts moderation={view.moderation} />
+        <ProfileHeaderAlerts moderation={moderation} />
       </View>
 
       {!isProfilePreview && (
         <ProfileHeaderSuggestedFollows
-          actorDid={view.did}
+          actorDid={profile.did}
           active={showSuggestedFollows}
           requestDismiss={() => setShowSuggestedFollows(!showSuggestedFollows)}
         />
@@ -570,7 +630,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
           onPress={onPressBack}
           hitSlop={BACK_HITSLOP}
           accessibilityRole="button"
-          accessibilityLabel="Back"
+          accessibilityLabel={_(msg`Back`)}
           accessibilityHint="">
           <View style={styles.backBtnWrapper}>
             <BlurView style={styles.backBtn} blurType="dark">
@@ -583,20 +643,21 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
         testID="profileHeaderAviButton"
         onPress={onPressAvi}
         accessibilityRole="image"
-        accessibilityLabel={`View ${view.handle}'s avatar`}
+        accessibilityLabel={`View ${profile.handle}'s avatar`}
         accessibilityHint="">
         <View
           style={[pal.view, {borderColor: pal.colors.background}, styles.avi]}>
           <UserAvatar
             size={80}
-            avatar={view.avatar}
-            moderation={view.moderation.avatar}
+            avatar={profile.avatar}
+            moderation={moderation.avatar}
           />
         </View>
       </TouchableWithoutFeedback>
     </View>
   )
-})
+}
+ProfileHeaderLoaded = memo(ProfileHeaderLoaded)
 
 const styles = StyleSheet.create({
   banner: {