about summary refs log tree commit diff
path: root/src/view
diff options
context:
space:
mode:
Diffstat (limited to 'src/view')
-rw-r--r--src/view/com/auth/LoggedOut.tsx42
-rw-r--r--src/view/com/feeds/FeedSourceCard.tsx3
-rw-r--r--src/view/com/notifications/FeedItem.tsx87
-rw-r--r--src/view/com/profile/FollowButton.tsx11
-rw-r--r--src/view/com/profile/ProfileCard.tsx6
-rw-r--r--src/view/com/profile/ProfileSubpageHeader.tsx10
-rw-r--r--src/view/screens/Home.tsx2
-rw-r--r--src/view/screens/Profile.tsx115
-rw-r--r--src/view/screens/Storybook/Icons.tsx8
-rw-r--r--src/view/shell/desktop/LeftNav.tsx8
10 files changed, 210 insertions, 82 deletions
diff --git a/src/view/com/auth/LoggedOut.tsx b/src/view/com/auth/LoggedOut.tsx
index c8c81dd77..29127ec45 100644
--- a/src/view/com/auth/LoggedOut.tsx
+++ b/src/view/com/auth/LoggedOut.tsx
@@ -7,7 +7,6 @@ import {useNavigation} from '@react-navigation/native'
 
 import {useAnalytics} from '#/lib/analytics/analytics'
 import {usePalette} from '#/lib/hooks/usePalette'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
 import {logEvent} from '#/lib/statsig/statsig'
 import {s} from '#/lib/styles'
 import {isIOS, isNative} from '#/platform/detection'
@@ -22,13 +21,16 @@ import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
 import {Text} from '#/view/com/util/text/Text'
 import {Login} from '#/screens/Login'
 import {Signup} from '#/screens/Signup'
+import {LandingScreen} from '#/screens/StarterPack/StarterPackLandingScreen'
 import {SplashScreen} from './SplashScreen'
 
 enum ScreenState {
   S_LoginOrCreateAccount,
   S_Login,
   S_CreateAccount,
+  S_StarterPack,
 }
+export {ScreenState as LoggedOutScreenState}
 
 export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
   const {hasSession} = useSession()
@@ -37,18 +39,21 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
   const setMinimalShellMode = useSetMinimalShellMode()
   const {screen} = useAnalytics()
   const {requestedAccountSwitchTo} = useLoggedOutView()
-  const [screenState, setScreenState] = React.useState<ScreenState>(
-    requestedAccountSwitchTo
-      ? requestedAccountSwitchTo === 'new'
-        ? ScreenState.S_CreateAccount
-        : ScreenState.S_Login
-      : ScreenState.S_LoginOrCreateAccount,
-  )
-  const {isMobile} = useWebMediaQueries()
+  const [screenState, setScreenState] = React.useState<ScreenState>(() => {
+    if (requestedAccountSwitchTo === 'new') {
+      return ScreenState.S_CreateAccount
+    } else if (requestedAccountSwitchTo === 'starterpack') {
+      return ScreenState.S_StarterPack
+    } else if (requestedAccountSwitchTo != null) {
+      return ScreenState.S_Login
+    } else {
+      return ScreenState.S_LoginOrCreateAccount
+    }
+  })
   const {clearRequestedAccount} = useLoggedOutViewControls()
   const navigation = useNavigation<NavigationProp>()
-  const isFirstScreen = screenState === ScreenState.S_LoginOrCreateAccount
 
+  const isFirstScreen = screenState === ScreenState.S_LoginOrCreateAccount
   React.useEffect(() => {
     screen('Login')
     setMinimalShellMode(true)
@@ -66,18 +71,9 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
   }, [navigation])
 
   return (
-    <View
-      testID="noSessionView"
-      style={[
-        s.hContentRegion,
-        pal.view,
-        {
-          // only needed if dismiss button is present
-          paddingTop: onDismiss && isMobile ? 40 : 0,
-        },
-      ]}>
+    <View testID="noSessionView" style={[s.hContentRegion, pal.view]}>
       <ErrorBoundary>
-        {onDismiss ? (
+        {onDismiss && screenState === ScreenState.S_LoginOrCreateAccount ? (
           <Pressable
             accessibilityHint={_(msg`Go back`)}
             accessibilityLabel={_(msg`Go back`)}
@@ -132,7 +128,9 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
           </Pressable>
         ) : null}
 
-        {screenState === ScreenState.S_LoginOrCreateAccount ? (
+        {screenState === ScreenState.S_StarterPack ? (
+          <LandingScreen setScreenState={setScreenState} />
+        ) : screenState === ScreenState.S_LoginOrCreateAccount ? (
           <SplashScreen
             onPressSignin={() => {
               setScreenState(ScreenState.S_Login)
diff --git a/src/view/com/feeds/FeedSourceCard.tsx b/src/view/com/feeds/FeedSourceCard.tsx
index a61789434..d216849c5 100644
--- a/src/view/com/feeds/FeedSourceCard.tsx
+++ b/src/view/com/feeds/FeedSourceCard.tsx
@@ -329,6 +329,9 @@ const styles = StyleSheet.create({
     flex: 1,
     gap: 14,
   },
+  border: {
+    borderTopWidth: hairlineWidth,
+  },
   headerContainer: {
     flexDirection: 'row',
   },
diff --git a/src/view/com/notifications/FeedItem.tsx b/src/view/com/notifications/FeedItem.tsx
index 9cd7a2917..2f8d65a1d 100644
--- a/src/view/com/notifications/FeedItem.tsx
+++ b/src/view/com/notifications/FeedItem.tsx
@@ -52,7 +52,16 @@ import {TimeElapsed} from '../util/TimeElapsed'
 import {PreviewableUserAvatar, UserAvatar} from '../util/UserAvatar'
 
 import hairlineWidth = StyleSheet.hairlineWidth
+import {useNavigation} from '@react-navigation/native'
+
 import {parseTenorGif} from '#/lib/strings/embed-player'
+import {logger} from '#/logger'
+import {NavigationProp} from 'lib/routes/types'
+import {DM_SERVICE_HEADERS} from 'state/queries/messages/const'
+import {useAgent} from 'state/session'
+import {Button, ButtonText} from '#/components/Button'
+import {StarterPack} from '#/components/icons/StarterPack'
+import {Notification as StarterPackCard} from '#/components/StarterPack/StarterPackCard'
 
 const MAX_AUTHORS = 5
 
@@ -89,7 +98,10 @@ let FeedItem = ({
     } else if (item.type === 'reply') {
       const urip = new AtUri(item.notification.uri)
       return `/profile/${urip.host}/post/${urip.rkey}`
-    } else if (item.type === 'feedgen-like') {
+    } else if (
+      item.type === 'feedgen-like' ||
+      item.type === 'starterpack-joined'
+    ) {
       if (item.subjectUri) {
         const urip = new AtUri(item.subjectUri)
         return `/profile/${urip.host}/feed/${urip.rkey}`
@@ -176,6 +188,13 @@ let FeedItem = ({
     icon = <PersonPlusIcon size="xl" style={{color: t.palette.primary_500}} />
   } else if (item.type === 'feedgen-like') {
     action = _(msg`liked your custom feed`)
+  } else if (item.type === 'starterpack-joined') {
+    icon = (
+      <View style={{height: 30, width: 30}}>
+        <StarterPack width={30} gradient="sky" />
+      </View>
+    )
+    action = _(msg`signed up with your starter pack`)
   } else {
     return null
   }
@@ -289,6 +308,20 @@ let FeedItem = ({
             showLikes
           />
         ) : null}
+        {item.type === 'starterpack-joined' ? (
+          <View>
+            <View
+              style={[
+                a.border,
+                a.p_sm,
+                a.rounded_sm,
+                a.mt_sm,
+                t.atoms.border_contrast_low,
+              ]}>
+              <StarterPackCard starterPack={item.subject} />
+            </View>
+          </View>
+        ) : null}
       </View>
     </Link>
   )
@@ -319,14 +352,63 @@ function ExpandListPressable({
   }
 }
 
+function SayHelloBtn({profile}: {profile: AppBskyActorDefs.ProfileViewBasic}) {
+  const {_} = useLingui()
+  const agent = useAgent()
+  const navigation = useNavigation<NavigationProp>()
+  const [isLoading, setIsLoading] = React.useState(false)
+
+  if (
+    profile.associated?.chat?.allowIncoming === 'none' ||
+    (profile.associated?.chat?.allowIncoming === 'following' &&
+      !profile.viewer?.followedBy)
+  ) {
+    return null
+  }
+
+  return (
+    <Button
+      label={_(msg`Say hello!`)}
+      variant="ghost"
+      color="primary"
+      size="xsmall"
+      style={[a.self_center, {marginLeft: 'auto'}]}
+      disabled={isLoading}
+      onPress={async () => {
+        try {
+          setIsLoading(true)
+          const res = await agent.api.chat.bsky.convo.getConvoForMembers(
+            {
+              members: [profile.did, agent.session!.did!],
+            },
+            {headers: DM_SERVICE_HEADERS},
+          )
+          navigation.navigate('MessagesConversation', {
+            conversation: res.data.convo.id,
+          })
+        } catch (e) {
+          logger.error('Failed to get conversation', {safeMessage: e})
+        } finally {
+          setIsLoading(false)
+        }
+      }}>
+      <ButtonText>
+        <Trans>Say hello!</Trans>
+      </ButtonText>
+    </Button>
+  )
+}
+
 function CondensedAuthorsList({
   visible,
   authors,
   onToggleAuthorsExpanded,
+  showDmButton = true,
 }: {
   visible: boolean
   authors: Author[]
   onToggleAuthorsExpanded: () => void
+  showDmButton?: boolean
 }) {
   const pal = usePalette('default')
   const {_} = useLingui()
@@ -355,7 +437,7 @@ function CondensedAuthorsList({
   }
   if (authors.length === 1) {
     return (
-      <View style={styles.avis}>
+      <View style={[styles.avis]}>
         <PreviewableUserAvatar
           size={35}
           profile={authors[0].profile}
@@ -363,6 +445,7 @@ function CondensedAuthorsList({
           type={authors[0].profile.associated?.labeler ? 'labeler' : 'user'}
           accessible={false}
         />
+        {showDmButton ? <SayHelloBtn profile={authors[0].profile} /> : null}
       </View>
     )
   }
diff --git a/src/view/com/profile/FollowButton.tsx b/src/view/com/profile/FollowButton.tsx
index 7b090ffeb..8e63da85b 100644
--- a/src/view/com/profile/FollowButton.tsx
+++ b/src/view/com/profile/FollowButton.tsx
@@ -1,12 +1,13 @@
 import React from 'react'
 import {StyleProp, TextStyle, View} from 'react-native'
 import {AppBskyActorDefs} from '@atproto/api'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {Shadow} from '#/state/cache/types'
+import {useProfileFollowMutationQueue} from '#/state/queries/profile'
 import {Button, ButtonType} from '../util/forms/Button'
 import * as Toast from '../util/Toast'
-import {useProfileFollowMutationQueue} from '#/state/queries/profile'
-import {Shadow} from '#/state/cache/types'
-import {useLingui} from '@lingui/react'
-import {msg} from '@lingui/macro'
 
 export function FollowButton({
   unfollowedType = 'inverted',
@@ -19,7 +20,7 @@ export function FollowButton({
   followedType?: ButtonType
   profile: Shadow<AppBskyActorDefs.ProfileViewBasic>
   labelStyle?: StyleProp<TextStyle>
-  logContext: 'ProfileCard'
+  logContext: 'ProfileCard' | 'StarterPackProfilesList'
 }) {
   const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(
     profile,
diff --git a/src/view/com/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx
index a3cd5ca1b..d7ed0dd6a 100644
--- a/src/view/com/profile/ProfileCard.tsx
+++ b/src/view/com/profile/ProfileCard.tsx
@@ -251,12 +251,14 @@ export function ProfileCardWithFollowBtn({
   noBorder,
   followers,
   onPress,
+  logContext = 'ProfileCard',
 }: {
   profile: AppBskyActorDefs.ProfileViewBasic
   noBg?: boolean
   noBorder?: boolean
   followers?: AppBskyActorDefs.ProfileView[] | undefined
   onPress?: () => void
+  logContext?: 'ProfileCard' | 'StarterPackProfilesList'
 }) {
   const {currentAccount} = useSession()
   const isMe = profile.did === currentAccount?.did
@@ -271,7 +273,7 @@ export function ProfileCardWithFollowBtn({
         isMe
           ? undefined
           : profileShadow => (
-              <FollowButton profile={profileShadow} logContext="ProfileCard" />
+              <FollowButton profile={profileShadow} logContext={logContext} />
             )
       }
       onPress={onPress}
@@ -314,6 +316,7 @@ const styles = StyleSheet.create({
     paddingRight: 10,
   },
   details: {
+    justifyContent: 'center',
     paddingLeft: 54,
     paddingRight: 10,
     paddingBottom: 10,
@@ -339,7 +342,6 @@ const styles = StyleSheet.create({
 
   followedBy: {
     flexDirection: 'row',
-    alignItems: 'center',
     paddingLeft: 54,
     paddingRight: 20,
     marginBottom: 10,
diff --git a/src/view/com/profile/ProfileSubpageHeader.tsx b/src/view/com/profile/ProfileSubpageHeader.tsx
index edc6b75f9..ac5febcda 100644
--- a/src/view/com/profile/ProfileSubpageHeader.tsx
+++ b/src/view/com/profile/ProfileSubpageHeader.tsx
@@ -21,7 +21,9 @@ import {Text} from '../util/text/Text'
 import {UserAvatar, UserAvatarType} from '../util/UserAvatar'
 import {CenteredView} from '../util/Views'
 import hairlineWidth = StyleSheet.hairlineWidth
+
 import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
+import {StarterPack} from '#/components/icons/StarterPack'
 
 export function ProfileSubpageHeader({
   isLoading,
@@ -44,7 +46,7 @@ export function ProfileSubpageHeader({
         handle: string
       }
     | undefined
-  avatarType: UserAvatarType
+  avatarType: UserAvatarType | 'starter-pack'
 }>) {
   const setDrawerOpen = useSetDrawerOpen()
   const navigation = useNavigation<NavigationProp>()
@@ -127,7 +129,11 @@ export function ProfileSubpageHeader({
           accessibilityLabel={_(msg`View the avatar`)}
           accessibilityHint=""
           style={{width: 58}}>
-          <UserAvatar type={avatarType} size={58} avatar={avatar} />
+          {avatarType === 'starter-pack' ? (
+            <StarterPack width={58} gradient="sky" />
+          ) : (
+            <UserAvatar type={avatarType} size={58} avatar={avatar} />
+          )}
         </Pressable>
         <View style={{flex: 1}}>
           {isLoading ? (
diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx
index e49f2fbb2..dfadf9bbe 100644
--- a/src/view/screens/Home.tsx
+++ b/src/view/screens/Home.tsx
@@ -30,7 +30,7 @@ import {FollowingEndOfFeed} from 'view/com/posts/FollowingEndOfFeed'
 import {NoFeedsPinned} from '#/screens/Home/NoFeedsPinned'
 import {HomeHeader} from '../com/home/HomeHeader'
 
-type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'>
+type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home' | 'Start'>
 export function HomeScreen(props: Props) {
   const {data: preferences} = usePreferencesQuery()
   const {data: pinnedFeedInfos, isLoading: isPinnedFeedsLoading} =
diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx
index 734230c6c..946f6ac54 100644
--- a/src/view/screens/Profile.tsx
+++ b/src/view/screens/Profile.tsx
@@ -1,7 +1,8 @@
-import React, {useMemo} from 'react'
+import React, {useCallback, useMemo} from 'react'
 import {StyleSheet} from 'react-native'
 import {
   AppBskyActorDefs,
+  AppBskyGraphGetActorStarterPacks,
   moderateProfile,
   ModerationOpts,
   RichText as RichTextAPI,
@@ -9,7 +10,11 @@ import {
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useFocusEffect} from '@react-navigation/native'
-import {useQueryClient} from '@tanstack/react-query'
+import {
+  InfiniteData,
+  UseInfiniteQueryResult,
+  useQueryClient,
+} from '@tanstack/react-query'
 
 import {cleanError} from '#/lib/strings/errors'
 import {useProfileShadow} from '#/state/cache/profile-shadow'
@@ -22,18 +27,23 @@ import {useAgent, useSession} from '#/state/session'
 import {useSetDrawerSwipeDisabled, useSetMinimalShellMode} from '#/state/shell'
 import {useComposerControls} from '#/state/shell/composer'
 import {useAnalytics} from 'lib/analytics/analytics'
+import {IS_DEV, IS_TESTFLIGHT} from 'lib/app-info'
 import {useSetTitle} from 'lib/hooks/useSetTitle'
 import {ComposeIcon2} from 'lib/icons'
 import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
+import {useGate} from 'lib/statsig/statsig'
 import {combinedDisplayName} from 'lib/strings/display-names'
 import {isInvalidHandle} from 'lib/strings/handles'
 import {colors, s} from 'lib/styles'
+import {isWeb} from 'platform/detection'
 import {listenSoftReset} from 'state/events'
+import {useActorStarterPacksQuery} from 'state/queries/actor-starter-packs'
 import {PagerWithHeader} from 'view/com/pager/PagerWithHeader'
 import {ProfileHeader, ProfileHeaderLoading} from '#/screens/Profile/Header'
 import {ProfileFeedSection} from '#/screens/Profile/Sections/Feed'
 import {ProfileLabelsSection} from '#/screens/Profile/Sections/Labels'
 import {ScreenHider} from '#/components/moderation/ScreenHider'
+import {ProfileStarterPacks} from '#/components/StarterPack/ProfileStarterPacks'
 import {ExpoScrollForwarderView} from '../../../modules/expo-scroll-forwarder'
 import {ProfileFeedgens} from '../com/feeds/ProfileFeedgens'
 import {ProfileLists} from '../com/lists/ProfileLists'
@@ -69,6 +79,7 @@ export function ProfileScreen({route}: Props) {
   } = useProfileQuery({
     did: resolvedDid,
   })
+  const starterPacksQuery = useActorStarterPacksQuery({did: resolvedDid})
 
   const onPressTryAgain = React.useCallback(() => {
     if (resolveError) {
@@ -86,7 +97,7 @@ export function ProfileScreen({route}: Props) {
   }, [queryClient, profile?.viewer?.blockedBy, resolvedDid])
 
   // Most pushes will happen here, since we will have only placeholder data
-  if (isLoadingDid || isLoadingProfile) {
+  if (isLoadingDid || isLoadingProfile || starterPacksQuery.isLoading) {
     return (
       <CenteredView>
         <ProfileHeaderLoading />
@@ -108,6 +119,7 @@ export function ProfileScreen({route}: Props) {
     return (
       <ProfileScreenLoaded
         profile={profile}
+        starterPacksQuery={starterPacksQuery}
         moderationOpts={moderationOpts}
         isPlaceholderProfile={isPlaceholderProfile}
         hideBackButton={!!route.params.hideBackButton}
@@ -131,11 +143,16 @@ function ProfileScreenLoaded({
   isPlaceholderProfile,
   moderationOpts,
   hideBackButton,
+  starterPacksQuery,
 }: {
   profile: AppBskyActorDefs.ProfileViewDetailed
   moderationOpts: ModerationOpts
   hideBackButton: boolean
   isPlaceholderProfile: boolean
+  starterPacksQuery: UseInfiniteQueryResult<
+    InfiniteData<AppBskyGraphGetActorStarterPacks.OutputSchema, unknown>,
+    Error
+  >
 }) {
   const profile = useProfileShadow(profileUnshadowed)
   const {hasSession, currentAccount} = useSession()
@@ -153,6 +170,9 @@ function ProfileScreenLoaded({
   const [currentPage, setCurrentPage] = React.useState(0)
   const {_} = useLingui()
   const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
+  const gate = useGate()
+  const starterPacksEnabled =
+    IS_DEV || IS_TESTFLIGHT || (!isWeb && gate('starter_packs_enabled'))
 
   const [scrollViewTag, setScrollViewTag] = React.useState<number | null>(null)
 
@@ -162,6 +182,7 @@ function ProfileScreenLoaded({
   const likesSectionRef = React.useRef<SectionRef>(null)
   const feedsSectionRef = React.useRef<SectionRef>(null)
   const listsSectionRef = React.useRef<SectionRef>(null)
+  const starterPacksSectionRef = React.useRef<SectionRef>(null)
   const labelsSectionRef = React.useRef<SectionRef>(null)
 
   useSetTitle(combinedDisplayName(profile))
@@ -183,31 +204,23 @@ function ProfileScreenLoaded({
   const showMediaTab = !hasLabeler
   const showLikesTab = isMe
   const showFeedsTab = isMe || (profile.associated?.feedgens || 0) > 0
+  const showStarterPacksTab =
+    starterPacksEnabled &&
+    (isMe || !!starterPacksQuery.data?.pages?.[0].starterPacks.length)
   const showListsTab =
     hasSession && (isMe || (profile.associated?.lists || 0) > 0)
 
-  const sectionTitles = useMemo<string[]>(() => {
-    return [
-      showFiltersTab ? _(msg`Labels`) : undefined,
-      showListsTab && hasLabeler ? _(msg`Lists`) : undefined,
-      showPostsTab ? _(msg`Posts`) : undefined,
-      showRepliesTab ? _(msg`Replies`) : undefined,
-      showMediaTab ? _(msg`Media`) : undefined,
-      showLikesTab ? _(msg`Likes`) : undefined,
-      showFeedsTab ? _(msg`Feeds`) : undefined,
-      showListsTab && !hasLabeler ? _(msg`Lists`) : undefined,
-    ].filter(Boolean) as string[]
-  }, [
-    showPostsTab,
-    showRepliesTab,
-    showMediaTab,
-    showLikesTab,
-    showFeedsTab,
-    showListsTab,
-    showFiltersTab,
-    hasLabeler,
-    _,
-  ])
+  const sectionTitles = [
+    showFiltersTab ? _(msg`Labels`) : undefined,
+    showListsTab && hasLabeler ? _(msg`Lists`) : undefined,
+    showPostsTab ? _(msg`Posts`) : undefined,
+    showRepliesTab ? _(msg`Replies`) : undefined,
+    showMediaTab ? _(msg`Media`) : undefined,
+    showLikesTab ? _(msg`Likes`) : undefined,
+    showFeedsTab ? _(msg`Feeds`) : undefined,
+    showStarterPacksTab ? _(msg`Starter Packs`) : undefined,
+    showListsTab && !hasLabeler ? _(msg`Lists`) : undefined,
+  ].filter(Boolean) as string[]
 
   let nextIndex = 0
   let filtersIndex: number | null = null
@@ -216,6 +229,7 @@ function ProfileScreenLoaded({
   let mediaIndex: number | null = null
   let likesIndex: number | null = null
   let feedsIndex: number | null = null
+  let starterPacksIndex: number | null = null
   let listsIndex: number | null = null
   if (showFiltersTab) {
     filtersIndex = nextIndex++
@@ -235,11 +249,14 @@ function ProfileScreenLoaded({
   if (showFeedsTab) {
     feedsIndex = nextIndex++
   }
+  if (showStarterPacksTab) {
+    starterPacksIndex = nextIndex++
+  }
   if (showListsTab) {
     listsIndex = nextIndex++
   }
 
-  const scrollSectionToTop = React.useCallback(
+  const scrollSectionToTop = useCallback(
     (index: number) => {
       if (index === filtersIndex) {
         labelsSectionRef.current?.scrollToTop()
@@ -253,6 +270,8 @@ function ProfileScreenLoaded({
         likesSectionRef.current?.scrollToTop()
       } else if (index === feedsIndex) {
         feedsSectionRef.current?.scrollToTop()
+      } else if (index === starterPacksIndex) {
+        starterPacksSectionRef.current?.scrollToTop()
       } else if (index === listsIndex) {
         listsSectionRef.current?.scrollToTop()
       }
@@ -265,6 +284,7 @@ function ProfileScreenLoaded({
       likesIndex,
       feedsIndex,
       listsIndex,
+      starterPacksIndex,
     ],
   )
 
@@ -290,7 +310,7 @@ function ProfileScreenLoaded({
   // events
   // =
 
-  const onPressCompose = React.useCallback(() => {
+  const onPressCompose = () => {
     track('ProfileScreen:PressCompose')
     const mention =
       profile.handle === currentAccount?.handle ||
@@ -298,23 +318,20 @@ function ProfileScreenLoaded({
         ? undefined
         : profile.handle
     openComposer({mention})
-  }, [openComposer, currentAccount, track, profile])
+  }
 
-  const onPageSelected = React.useCallback((i: number) => {
+  const onPageSelected = (i: number) => {
     setCurrentPage(i)
-  }, [])
+  }
 
-  const onCurrentPageSelected = React.useCallback(
-    (index: number) => {
-      scrollSectionToTop(index)
-    },
-    [scrollSectionToTop],
-  )
+  const onCurrentPageSelected = (index: number) => {
+    scrollSectionToTop(index)
+  }
 
   // rendering
   // =
 
-  const renderHeader = React.useCallback(() => {
+  const renderHeader = () => {
     return (
       <ExpoScrollForwarderView scrollViewTag={scrollViewTag}>
         <ProfileHeader
@@ -327,16 +344,7 @@ function ProfileScreenLoaded({
         />
       </ExpoScrollForwarderView>
     )
-  }, [
-    scrollViewTag,
-    profile,
-    labelerInfo,
-    hasDescription,
-    descriptionRT,
-    moderationOpts,
-    hideBackButton,
-    showPlaceholder,
-  ])
+  }
 
   return (
     <ScreenHider
@@ -442,6 +450,19 @@ function ProfileScreenLoaded({
               />
             )
           : null}
+        {showStarterPacksTab
+          ? ({headerHeight, isFocused, scrollElRef}) => (
+              <ProfileStarterPacks
+                ref={starterPacksSectionRef}
+                isMe={isMe}
+                starterPacksQuery={starterPacksQuery}
+                scrollElRef={scrollElRef as ListRef}
+                headerOffset={headerHeight}
+                enabled={isFocused}
+                setScrollViewTag={setScrollViewTag}
+              />
+            )
+          : null}
         {showListsTab && !profile.associated?.labeler
           ? ({headerHeight, isFocused, scrollElRef}) => (
               <ProfileLists
diff --git a/src/view/screens/Storybook/Icons.tsx b/src/view/screens/Storybook/Icons.tsx
index bff1fdc9b..9de126d6b 100644
--- a/src/view/screens/Storybook/Icons.tsx
+++ b/src/view/screens/Storybook/Icons.tsx
@@ -45,6 +45,14 @@ export function Icons() {
         <Loader size="lg" fill={t.atoms.text.color} />
         <Loader size="xl" fill={t.atoms.text.color} />
       </View>
+
+      <View style={[a.flex_row, a.gap_xl]}>
+        <Globe size="xs" gradient="sky" />
+        <Globe size="sm" gradient="sky" />
+        <Globe size="md" gradient="sky" />
+        <Globe size="lg" gradient="sky" />
+        <Globe size="xl" gradient="sky" />
+      </View>
     </View>
   )
 }
diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx
index 9b2b4922a..ca8073f57 100644
--- a/src/view/shell/desktop/LeftNav.tsx
+++ b/src/view/shell/desktop/LeftNav.tsx
@@ -100,12 +100,18 @@ function ProfileCard() {
   )
 }
 
+const HIDDEN_BACK_BNT_ROUTES = ['StarterPackWizard', 'StarterPackEdit']
+
 function BackBtn() {
   const {isTablet} = useWebMediaQueries()
   const pal = usePalette('default')
   const navigation = useNavigation<NavigationProp>()
   const {_} = useLingui()
-  const shouldShow = useNavigationState(state => !isStateAtTabRoot(state))
+  const shouldShow = useNavigationState(
+    state =>
+      !isStateAtTabRoot(state) &&
+      !HIDDEN_BACK_BNT_ROUTES.includes(getCurrentRoute(state).name),
+  )
 
   const onPressBack = React.useCallback(() => {
     if (navigation.canGoBack()) {