about summary refs log tree commit diff
diff options
context:
space:
mode:
authorEric Bailey <git@esb.lol>2024-12-12 11:48:26 -0600
committerGitHub <noreply@github.com>2024-12-12 11:48:26 -0600
commit2808f8b73da4791e0f3cb85b03403738e5bf6b63 (patch)
treea254debcb1b11c51489aebe3fcd0f91d72049cf9
parentffc63dc85fc191a51c3dc12c1afcd250f95036d5 (diff)
downloadvoidsky-2808f8b73da4791e0f3cb85b03403738e5bf6b63.tar.zst
New profile feed header (#7056)
* Init hacking

* Lil baby button checkpoint

* Playing around

* Revert "Playing around"

This reverts commit f58a7fafa12269035d440cfa2d8cb1dbd562305f.

* Mostly there

* Cleanups

* Cleanup

* Fix report dialog nesting

* Remove transform on native

* Rename header

* Fix layout, overflowing FAB buttons

* Remove hack

* Couple of fixes

* Keep Pin primary CTA (#7061)

* Update src/screens/Profile/components/ProfileFeedHeader.tsx

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* Simplify, use old string

* Wrap Trans better

---------

Co-authored-by: dan <dan.abramov@gmail.com>
Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
-rw-r--r--assets/icons/pin_filled_stroke2_corner0_rounded.svg1
-rw-r--r--src/Navigation.tsx2
-rw-r--r--src/alf/themes.ts4
-rw-r--r--src/alf/types.ts1
-rw-r--r--src/components/Layout/index.tsx4
-rw-r--r--src/components/icons/Pin.tsx4
-rw-r--r--src/screens/Profile/ProfileFeed/index.tsx227
-rw-r--r--src/screens/Profile/components/ProfileFeedHeader.tsx534
-rw-r--r--src/view/screens/ProfileFeed.tsx621
9 files changed, 775 insertions, 623 deletions
diff --git a/assets/icons/pin_filled_stroke2_corner0_rounded.svg b/assets/icons/pin_filled_stroke2_corner0_rounded.svg
new file mode 100644
index 000000000..a2e71b967
--- /dev/null
+++ b/assets/icons/pin_filled_stroke2_corner0_rounded.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M7.5 2a1 1 0 0 0-1 1v3.997a6.25 6.25 0 0 1-1.83 4.42l-.377.376A1 1 0 0 0 4 12.5V15a1 1 0 0 0 1 1h6v5a1 1 0 1 0 2 0v-5h6a1 1 0 0 0 1-1v-2.5a1 1 0 0 0-.293-.707l-.376-.377a6.25 6.25 0 0 1-1.831-4.42V3.001a1 1 0 0 0-1-1h-9Z"/></svg>
diff --git a/src/Navigation.tsx b/src/Navigation.tsx
index cf0021526..7443128d2 100644
--- a/src/Navigation.tsx
+++ b/src/Navigation.tsx
@@ -55,7 +55,6 @@ import {NotificationsScreen} from '#/view/screens/Notifications'
 import {PostThreadScreen} from '#/view/screens/PostThread'
 import {PrivacyPolicyScreen} from '#/view/screens/PrivacyPolicy'
 import {ProfileScreen} from '#/view/screens/Profile'
-import {ProfileFeedScreen} from '#/view/screens/ProfileFeed'
 import {ProfileFeedLikedByScreen} from '#/view/screens/ProfileFeedLikedBy'
 import {ProfileListScreen} from '#/view/screens/ProfileList'
 import {SavedFeeds} from '#/view/screens/SavedFeeds'
@@ -75,6 +74,7 @@ import {PostLikedByScreen} from '#/screens/Post/PostLikedBy'
 import {PostQuotesScreen} from '#/screens/Post/PostQuotes'
 import {PostRepostedByScreen} from '#/screens/Post/PostRepostedBy'
 import {ProfileKnownFollowersScreen} from '#/screens/Profile/KnownFollowers'
+import {ProfileFeedScreen} from '#/screens/Profile/ProfileFeed'
 import {ProfileFollowersScreen} from '#/screens/Profile/ProfileFollowers'
 import {ProfileFollowsScreen} from '#/screens/Profile/ProfileFollows'
 import {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLikedBy'
diff --git a/src/alf/themes.ts b/src/alf/themes.ts
index 0cfe09aad..cb97a7065 100644
--- a/src/alf/themes.ts
+++ b/src/alf/themes.ts
@@ -60,6 +60,7 @@ export function createThemes({
   dim: Theme
 } {
   const color = {
+    like: '#ec4899',
     trueBlack: '#000000',
 
     gray_0: `hsl(${hues.primary}, 20%, ${defaultScale[14]}%)`,
@@ -124,6 +125,7 @@ export function createThemes({
   const lightPalette = {
     white: color.gray_0,
     black: color.gray_1000,
+    like: color.like,
 
     contrast_25: color.gray_25,
     contrast_50: color.gray_50,
@@ -185,6 +187,7 @@ export function createThemes({
   const darkPalette: Palette = {
     white: color.gray_25,
     black: color.trueBlack,
+    like: color.like,
 
     contrast_25: color.gray_975,
     contrast_50: color.gray_950,
@@ -246,6 +249,7 @@ export function createThemes({
   const dimPalette: Palette = {
     ...darkPalette,
     black: `hsl(${hues.primary}, 28%, ${dimScale[0]}%)`,
+    like: color.like,
 
     contrast_25: `hsl(${hues.primary}, 28%, ${dimScale[1]}%)`,
     contrast_50: `hsl(${hues.primary}, 28%, ${dimScale[2]}%)`,
diff --git a/src/alf/types.ts b/src/alf/types.ts
index 08ec59392..5bac690e2 100644
--- a/src/alf/types.ts
+++ b/src/alf/types.ts
@@ -12,6 +12,7 @@ export type ThemeName = 'light' | 'dim' | 'dark'
 export type Palette = {
   white: string
   black: string
+  like: string
 
   contrast_25: string
   contrast_50: string
diff --git a/src/components/Layout/index.tsx b/src/components/Layout/index.tsx
index d08505fbf..8532cbbb4 100644
--- a/src/components/Layout/index.tsx
+++ b/src/components/Layout/index.tsx
@@ -21,6 +21,7 @@ export * as Header from '#/components/Layout/Header'
 
 export type ScreenProps = React.ComponentProps<typeof View> & {
   style?: StyleProp<ViewStyle>
+  noInsetTop?: boolean
 }
 
 /**
@@ -28,6 +29,7 @@ export type ScreenProps = React.ComponentProps<typeof View> & {
  */
 export const Screen = React.memo(function Screen({
   style,
+  noInsetTop,
   ...props
 }: ScreenProps) {
   const {top} = useSafeAreaInsets()
@@ -35,7 +37,7 @@ export const Screen = React.memo(function Screen({
     <>
       {isWeb && <WebCenterBorders />}
       <View
-        style={[a.util_screen_outer, {paddingTop: top}, style]}
+        style={[a.util_screen_outer, {paddingTop: noInsetTop ? 0 : top}, style]}
         {...props}
       />
     </>
diff --git a/src/components/icons/Pin.tsx b/src/components/icons/Pin.tsx
index 03dbbac90..d1c37f39a 100644
--- a/src/components/icons/Pin.tsx
+++ b/src/components/icons/Pin.tsx
@@ -3,3 +3,7 @@ import {createSinglePathSVG} from './TEMPLATE'
 export const Pin_Stroke2_Corner0_Rounded = createSinglePathSVG({
   path: 'M6.5 3a1 1 0 0 1 1-1h9a1 1 0 0 1 1 1v3.997a6.25 6.25 0 0 0 1.83 4.42l.377.376A1 1 0 0 1 20 12.5V15a1 1 0 0 1-1 1h-6v5a1 1 0 1 1-2 0v-5H5a1 1 0 0 1-1-1v-2.5a1 1 0 0 1 .293-.707l.376-.377A6.25 6.25 0 0 0 6.5 6.996V3.001Zm2 1v2.997a8.25 8.25 0 0 1-2.416 5.834L6 12.914V14h12v-1.086l-.084-.083A8.25 8.25 0 0 1 15.5 6.997V4h-7Z',
 })
+
+export const Pin_Filled_Corner0_Rounded = createSinglePathSVG({
+  path: 'M7.5 2a1 1 0 0 0-1 1v3.997a6.25 6.25 0 0 1-1.83 4.42l-.377.376A1 1 0 0 0 4 12.5V15a1 1 0 0 0 1 1h6v5a1 1 0 1 0 2 0v-5h6a1 1 0 0 0 1-1v-2.5a1 1 0 0 0-.293-.707l-.376-.377a6.25 6.25 0 0 1-1.831-4.42V3.001a1 1 0 0 0-1-1h-9Z',
+})
diff --git a/src/screens/Profile/ProfileFeed/index.tsx b/src/screens/Profile/ProfileFeed/index.tsx
new file mode 100644
index 000000000..7d48b5ac1
--- /dev/null
+++ b/src/screens/Profile/ProfileFeed/index.tsx
@@ -0,0 +1,227 @@
+import React, {useCallback, useMemo} from 'react'
+import {StyleSheet, View} from 'react-native'
+import {useAnimatedRef} from 'react-native-reanimated'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useIsFocused, useNavigation} from '@react-navigation/native'
+import {NativeStackScreenProps} from '@react-navigation/native-stack'
+import {useQueryClient} from '@tanstack/react-query'
+
+import {usePalette} from '#/lib/hooks/usePalette'
+import {useSetTitle} from '#/lib/hooks/useSetTitle'
+import {ComposeIcon2} from '#/lib/icons'
+import {CommonNavigatorParams} from '#/lib/routes/types'
+import {NavigationProp} from '#/lib/routes/types'
+import {makeRecordUri} from '#/lib/strings/url-helpers'
+import {s} from '#/lib/styles'
+import {isNative} from '#/platform/detection'
+import {listenSoftReset} from '#/state/events'
+import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback'
+import {FeedSourceFeedInfo, useFeedSourceInfoQuery} from '#/state/queries/feed'
+import {FeedDescriptor} from '#/state/queries/post-feed'
+import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
+import {
+  usePreferencesQuery,
+  UsePreferencesQueryResponse,
+} from '#/state/queries/preferences'
+import {useResolveUriQuery} from '#/state/queries/resolve-uri'
+import {truncateAndInvalidate} from '#/state/queries/util'
+import {useSession} from '#/state/session'
+import {useComposerControls} from '#/state/shell/composer'
+import {PostFeed} from '#/view/com/posts/PostFeed'
+import {EmptyState} from '#/view/com/util/EmptyState'
+import {FAB} from '#/view/com/util/fab/FAB'
+import {Button} from '#/view/com/util/forms/Button'
+import {ListRef} from '#/view/com/util/List'
+import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn'
+import {LoadingScreen} from '#/view/com/util/LoadingScreen'
+import {Text} from '#/view/com/util/text/Text'
+import {ProfileFeedHeader} from '#/screens/Profile/components/ProfileFeedHeader'
+import * as Layout from '#/components/Layout'
+
+type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFeed'>
+export function ProfileFeedScreen(props: Props) {
+  const {rkey, name: handleOrDid} = props.route.params
+
+  const pal = usePalette('default')
+  const {_} = useLingui()
+  const navigation = useNavigation<NavigationProp>()
+
+  const uri = useMemo(
+    () => makeRecordUri(handleOrDid, 'app.bsky.feed.generator', rkey),
+    [rkey, handleOrDid],
+  )
+  const {error, data: resolvedUri} = useResolveUriQuery(uri)
+
+  const onPressBack = React.useCallback(() => {
+    if (navigation.canGoBack()) {
+      navigation.goBack()
+    } else {
+      navigation.navigate('Home')
+    }
+  }, [navigation])
+
+  if (error) {
+    return (
+      <Layout.Screen testID="profileFeedScreenError">
+        <Layout.Content>
+          <View style={[pal.view, pal.border, styles.notFoundContainer]}>
+            <Text type="title-lg" style={[pal.text, s.mb10]}>
+              <Trans>Could not load feed</Trans>
+            </Text>
+            <Text type="md" style={[pal.text, s.mb20]}>
+              {error.toString()}
+            </Text>
+
+            <View style={{flexDirection: 'row'}}>
+              <Button
+                type="default"
+                accessibilityLabel={_(msg`Go back`)}
+                accessibilityHint={_(msg`Returns to previous page`)}
+                onPress={onPressBack}
+                style={{flexShrink: 1}}>
+                <Text type="button" style={pal.text}>
+                  <Trans>Go Back</Trans>
+                </Text>
+              </Button>
+            </View>
+          </View>
+        </Layout.Content>
+      </Layout.Screen>
+    )
+  }
+
+  return resolvedUri ? (
+    <Layout.Screen noInsetTop>
+      <ProfileFeedScreenIntermediate feedUri={resolvedUri.uri} />
+    </Layout.Screen>
+  ) : (
+    <Layout.Screen>
+      <LoadingScreen />
+    </Layout.Screen>
+  )
+}
+
+function ProfileFeedScreenIntermediate({feedUri}: {feedUri: string}) {
+  const {data: preferences} = usePreferencesQuery()
+  const {data: info} = useFeedSourceInfoQuery({uri: feedUri})
+
+  if (!preferences || !info) {
+    return <LoadingScreen />
+  }
+
+  return (
+    <ProfileFeedScreenInner
+      preferences={preferences}
+      feedInfo={info as FeedSourceFeedInfo}
+    />
+  )
+}
+
+export function ProfileFeedScreenInner({
+  feedInfo,
+}: {
+  preferences: UsePreferencesQueryResponse
+  feedInfo: FeedSourceFeedInfo
+}) {
+  const {_} = useLingui()
+  const {hasSession} = useSession()
+  const {openComposer} = useComposerControls()
+  const isScreenFocused = useIsFocused()
+
+  useSetTitle(feedInfo?.displayName)
+
+  const feed = `feedgen|${feedInfo.uri}` as FeedDescriptor
+
+  const [hasNew, setHasNew] = React.useState(false)
+  const [isScrolledDown, setIsScrolledDown] = React.useState(false)
+  const queryClient = useQueryClient()
+  const feedFeedback = useFeedFeedback(feed, hasSession)
+  const scrollElRef = useAnimatedRef() as ListRef
+
+  const onScrollToTop = useCallback(() => {
+    scrollElRef.current?.scrollToOffset({
+      animated: isNative,
+      offset: 0, // -headerHeight,
+    })
+    truncateAndInvalidate(queryClient, FEED_RQKEY(feed))
+    setHasNew(false)
+  }, [scrollElRef, queryClient, feed, setHasNew])
+
+  React.useEffect(() => {
+    if (!isScreenFocused) {
+      return
+    }
+    return listenSoftReset(onScrollToTop)
+  }, [onScrollToTop, isScreenFocused])
+
+  const renderPostsEmpty = useCallback(() => {
+    return <EmptyState icon="hashtag" message={_(msg`This feed is empty.`)} />
+  }, [_])
+
+  return (
+    <>
+      <ProfileFeedHeader info={feedInfo} />
+
+      <FeedFeedbackProvider value={feedFeedback}>
+        <PostFeed
+          feed={feed}
+          pollInterval={60e3}
+          disablePoll={hasNew}
+          onHasNew={setHasNew}
+          scrollElRef={scrollElRef}
+          onScrolledDownChange={setIsScrolledDown}
+          renderEmptyState={renderPostsEmpty}
+        />
+      </FeedFeedbackProvider>
+
+      {(isScrolledDown || hasNew) && (
+        <LoadLatestBtn
+          onPress={onScrollToTop}
+          label={_(msg`Load new posts`)}
+          showIndicator={hasNew}
+        />
+      )}
+
+      {hasSession && (
+        <FAB
+          testID="composeFAB"
+          onPress={() => openComposer({})}
+          icon={
+            <ComposeIcon2
+              strokeWidth={1.5}
+              size={29}
+              style={{color: 'white'}}
+            />
+          }
+          accessibilityRole="button"
+          accessibilityLabel={_(msg`New post`)}
+          accessibilityHint=""
+        />
+      )}
+    </>
+  )
+}
+
+const styles = StyleSheet.create({
+  btn: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    gap: 6,
+    paddingVertical: 7,
+    paddingHorizontal: 14,
+    borderRadius: 50,
+    marginLeft: 6,
+  },
+  notFoundContainer: {
+    margin: 10,
+    paddingHorizontal: 18,
+    paddingVertical: 14,
+    borderRadius: 6,
+  },
+  aboutSectionContainer: {
+    paddingVertical: 4,
+    paddingHorizontal: 16,
+    gap: 12,
+  },
+})
diff --git a/src/screens/Profile/components/ProfileFeedHeader.tsx b/src/screens/Profile/components/ProfileFeedHeader.tsx
new file mode 100644
index 000000000..0154d535c
--- /dev/null
+++ b/src/screens/Profile/components/ProfileFeedHeader.tsx
@@ -0,0 +1,534 @@
+import React from 'react'
+import {View} from 'react-native'
+import {useSafeAreaInsets} from 'react-native-safe-area-context'
+import {AtUri} from '@atproto/api'
+import {msg, Plural, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {useHaptics} from '#/lib/haptics'
+import {makeProfileLink} from '#/lib/routes/links'
+import {makeCustomFeedLink} from '#/lib/routes/links'
+import {shareUrl} from '#/lib/sharing'
+import {sanitizeHandle} from '#/lib/strings/handles'
+import {toShareUrl} from '#/lib/strings/url-helpers'
+import {logger} from '#/logger'
+import {FeedSourceFeedInfo} from '#/state/queries/feed'
+import {useLikeMutation, useUnlikeMutation} from '#/state/queries/like'
+import {
+  useAddSavedFeedsMutation,
+  usePreferencesQuery,
+  useRemoveFeedMutation,
+  useUpdateSavedFeedsMutation,
+} from '#/state/queries/preferences'
+import {useSession} from '#/state/session'
+import {formatCount} from '#/view/com/util/numeric/format'
+import * as Toast from '#/view/com/util/Toast'
+import {UserAvatar} from '#/view/com/util/UserAvatar'
+import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
+import {Button, ButtonIcon, ButtonText} from '#/components/Button'
+import * as Dialog from '#/components/Dialog'
+import {Divider} from '#/components/Divider'
+import {useRichText} from '#/components/hooks/useRichText'
+import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox'
+import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron'
+import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
+import {
+  Heart2_Filled_Stroke2_Corner0_Rounded as HeartFilled,
+  Heart2_Stroke2_Corner0_Rounded as Heart,
+} from '#/components/icons/Heart2'
+import {
+  Pin_Filled_Corner0_Rounded as PinFilled,
+  Pin_Stroke2_Corner0_Rounded as Pin,
+} from '#/components/icons/Pin'
+import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
+import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
+import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
+import * as Layout from '#/components/Layout'
+import {InlineLinkText} from '#/components/Link'
+import {Loader} from '#/components/Loader'
+import * as Menu from '#/components/Menu'
+import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog'
+import {RichText} from '#/components/RichText'
+import {Text} from '#/components/Typography'
+
+export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) {
+  const t = useTheme()
+  const {_, i18n} = useLingui()
+  const {hasSession} = useSession()
+  const {gtPhone, gtMobile} = useBreakpoints()
+  const {top} = useSafeAreaInsets()
+  const infoControl = Dialog.useDialogControl()
+  const playHaptic = useHaptics()
+
+  const {data: preferences} = usePreferencesQuery()
+
+  const [likeUri, setLikeUri] = React.useState(info.likeUri || '')
+  const isLiked = !!likeUri
+  const likeCount =
+    isLiked && likeUri ? (info.likeCount || 0) + 1 : info.likeCount || 0
+
+  const {mutateAsync: addSavedFeeds, isPending: isAddSavedFeedPending} =
+    useAddSavedFeedsMutation()
+  const {mutateAsync: removeFeed, isPending: isRemovePending} =
+    useRemoveFeedMutation()
+  const {mutateAsync: updateSavedFeeds, isPending: isUpdateFeedPending} =
+    useUpdateSavedFeedsMutation()
+
+  const isFeedStateChangePending =
+    isAddSavedFeedPending || isRemovePending || isUpdateFeedPending
+  const savedFeedConfig = preferences?.savedFeeds?.find(
+    f => f.value === info.uri,
+  )
+  const isSaved = Boolean(savedFeedConfig)
+  const isPinned = Boolean(savedFeedConfig?.pinned)
+
+  const onToggleSaved = React.useCallback(async () => {
+    try {
+      playHaptic()
+
+      if (savedFeedConfig) {
+        await removeFeed(savedFeedConfig)
+        Toast.show(_(msg`Removed from your feeds`))
+      } else {
+        await addSavedFeeds([
+          {
+            type: 'feed',
+            value: info.uri,
+            pinned: false,
+          },
+        ])
+        Toast.show(_(msg`Saved to your feeds`))
+      }
+    } catch (err) {
+      Toast.show(
+        _(
+          msg`There was an issue updating your feeds, please check your internet connection and try again.`,
+        ),
+        'xmark',
+      )
+      logger.error('Failed to update feeds', {message: err})
+    }
+  }, [_, playHaptic, info, removeFeed, addSavedFeeds, savedFeedConfig])
+
+  const onTogglePinned = React.useCallback(async () => {
+    try {
+      playHaptic()
+
+      if (savedFeedConfig) {
+        const pinned = !savedFeedConfig.pinned
+        await updateSavedFeeds([
+          {
+            ...savedFeedConfig,
+            pinned,
+          },
+        ])
+
+        if (pinned) {
+          Toast.show(_(msg`Pinned ${info.displayName} to Home`))
+        } else {
+          Toast.show(_(msg`Unpinned ${info.displayName} from Home`))
+        }
+      } else {
+        await addSavedFeeds([
+          {
+            type: 'feed',
+            value: info.uri,
+            pinned: true,
+          },
+        ])
+        Toast.show(_(msg`Pinned ${info.displayName} to Home`))
+      }
+    } catch (e) {
+      Toast.show(_(msg`There was an issue contacting the server`), 'xmark')
+      logger.error('Failed to toggle pinned feed', {message: e})
+    }
+  }, [playHaptic, info, _, savedFeedConfig, updateSavedFeeds, addSavedFeeds])
+
+  return (
+    <>
+      <Layout.Center
+        style={[
+          t.atoms.bg,
+          a.z_10,
+          {paddingTop: top},
+          web([a.sticky, a.z_10, {top: 0}]),
+        ]}>
+        <Layout.Header.Outer>
+          <Layout.Header.BackButton />
+          <Layout.Header.Content align="left">
+            <Button
+              label={_(msg`Open feed info screen`)}
+              style={[
+                a.justify_start,
+                {
+                  paddingVertical: 6,
+                  paddingHorizontal: 8,
+                  paddingRight: 12,
+                },
+              ]}
+              onPress={() => {
+                playHaptic()
+                infoControl.open()
+              }}>
+              {({hovered, pressed}) => (
+                <>
+                  <View
+                    style={[
+                      a.absolute,
+                      a.inset_0,
+                      a.rounded_sm,
+                      a.transition_transform,
+                      t.atoms.bg_contrast_25,
+                      pressed && t.atoms.bg_contrast_50,
+                      hovered && {
+                        transform: [{scaleX: 1.01}, {scaleY: 1.1}],
+                      },
+                    ]}
+                  />
+
+                  <View
+                    style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
+                    {info.avatar && (
+                      <UserAvatar size={32} type="algo" avatar={info.avatar} />
+                    )}
+
+                    <View style={[a.flex_1]}>
+                      <Text
+                        style={[
+                          a.text_md,
+                          a.font_heavy,
+                          a.leading_tight,
+                          gtMobile && a.text_xl,
+                        ]}
+                        numberOfLines={2}>
+                        {info.displayName}
+                      </Text>
+                      <View style={[a.flex_row, {gap: 6}]}>
+                        <Text
+                          style={[
+                            a.flex_shrink,
+                            a.text_xs,
+                            a.leading_snug,
+                            t.atoms.text_contrast_medium,
+                            gtPhone && a.text_sm,
+                          ]}
+                          numberOfLines={1}>
+                          {sanitizeHandle(info.creatorHandle, '@')}
+                        </Text>
+                        <View style={[a.flex_row, a.align_center, {gap: 2}]}>
+                          <HeartFilled
+                            size="xs"
+                            fill={
+                              likeUri
+                                ? t.palette.like
+                                : t.atoms.text_contrast_low.color
+                            }
+                          />
+                          <Text
+                            style={[
+                              a.text_xs,
+                              a.leading_snug,
+                              t.atoms.text_contrast_medium,
+                              gtPhone && a.text_sm,
+                            ]}
+                            numberOfLines={1}>
+                            {formatCount(i18n, likeCount)}
+                          </Text>
+                        </View>
+                      </View>
+                    </View>
+
+                    <ChevronDown
+                      size="md"
+                      fill={t.atoms.text_contrast_low.color}
+                    />
+                  </View>
+                </>
+              )}
+            </Button>
+          </Layout.Header.Content>
+
+          {hasSession && (
+            <Layout.Header.Slot>
+              {isPinned ? (
+                <Menu.Root>
+                  <Menu.Trigger label={_(msg`Open feed options menu`)}>
+                    {({props}) => {
+                      return (
+                        <Button
+                          {...props}
+                          label={_(msg`Open feed options menu`)}
+                          size="small"
+                          variant="ghost"
+                          shape="square"
+                          color="secondary">
+                          <PinFilled size="lg" fill={t.palette.primary_500} />
+                        </Button>
+                      )
+                    }}
+                  </Menu.Trigger>
+
+                  <Menu.Outer>
+                    <Menu.Item
+                      disabled={isFeedStateChangePending}
+                      label={_(msg`Unpin from home`)}
+                      onPress={onTogglePinned}>
+                      <Menu.ItemText>{_(msg`Unpin from home`)}</Menu.ItemText>
+                      <Menu.ItemIcon icon={X} position="right" />
+                    </Menu.Item>
+                    <Menu.Item
+                      disabled={isFeedStateChangePending}
+                      label={
+                        isSaved
+                          ? _(msg`Remove from my feeds`)
+                          : _(msg`Save to my feeds`)
+                      }
+                      onPress={onToggleSaved}>
+                      <Menu.ItemText>
+                        {isSaved
+                          ? _(msg`Remove from my feeds`)
+                          : _(msg`Save to my feeds`)}
+                      </Menu.ItemText>
+                      <Menu.ItemIcon
+                        icon={isSaved ? Trash : Plus}
+                        position="right"
+                      />
+                    </Menu.Item>
+                  </Menu.Outer>
+                </Menu.Root>
+              ) : (
+                <Button
+                  label={_(msg`Pin to Home`)}
+                  size="small"
+                  variant="ghost"
+                  shape="square"
+                  color="secondary"
+                  onPress={onTogglePinned}>
+                  <ButtonIcon icon={Pin} size="lg" />
+                </Button>
+              )}
+            </Layout.Header.Slot>
+          )}
+        </Layout.Header.Outer>
+      </Layout.Center>
+
+      <Dialog.Outer control={infoControl}>
+        <Dialog.Handle />
+        <Dialog.ScrollableInner
+          label={_(msg`Feed menu`)}
+          style={[gtMobile ? {width: 'auto', minWidth: 450} : a.w_full]}>
+          <DialogInner
+            info={info}
+            likeUri={likeUri}
+            setLikeUri={setLikeUri}
+            likeCount={likeCount}
+            isPinned={isPinned}
+            onTogglePinned={onTogglePinned}
+            isFeedStateChangePending={isFeedStateChangePending}
+          />
+        </Dialog.ScrollableInner>
+      </Dialog.Outer>
+    </>
+  )
+}
+
+function DialogInner({
+  info,
+  likeUri,
+  setLikeUri,
+  likeCount,
+  isPinned,
+  onTogglePinned,
+  isFeedStateChangePending,
+}: {
+  info: FeedSourceFeedInfo
+  likeUri: string
+  setLikeUri: (uri: string) => void
+  likeCount: number
+  isPinned: boolean
+  onTogglePinned: () => void
+  isFeedStateChangePending: boolean
+}) {
+  const t = useTheme()
+  const {_} = useLingui()
+  const {hasSession} = useSession()
+  const playHaptic = useHaptics()
+  const control = Dialog.useDialogContext()
+  const reportDialogControl = useReportDialogControl()
+  const [rt, loading] = useRichText(info.description.text)
+  const {mutateAsync: likeFeed, isPending: isLikePending} = useLikeMutation()
+  const {mutateAsync: unlikeFeed, isPending: isUnlikePending} =
+    useUnlikeMutation()
+
+  const isLiked = !!likeUri
+  const feedRkey = React.useMemo(() => new AtUri(info.uri).rkey, [info.uri])
+
+  const onToggleLiked = React.useCallback(async () => {
+    try {
+      playHaptic()
+
+      if (isLiked && likeUri) {
+        await unlikeFeed({uri: likeUri})
+        setLikeUri('')
+      } else {
+        const res = await likeFeed({uri: info.uri, cid: info.cid})
+        setLikeUri(res.uri)
+      }
+    } catch (err) {
+      Toast.show(
+        _(
+          msg`There was an issue contacting the server, please check your internet connection and try again.`,
+        ),
+        'xmark',
+      )
+      logger.error('Failed to toggle like', {message: err})
+    }
+  }, [playHaptic, isLiked, likeUri, unlikeFeed, setLikeUri, likeFeed, info, _])
+
+  const onPressShare = React.useCallback(() => {
+    playHaptic()
+    const url = toShareUrl(info.route.href)
+    shareUrl(url)
+  }, [info, playHaptic])
+
+  const onPressReport = React.useCallback(() => {
+    reportDialogControl.open()
+  }, [reportDialogControl])
+
+  return loading ? (
+    <Loader size="xl" />
+  ) : (
+    <View style={[a.gap_md]}>
+      <View style={[a.flex_row, a.align_center, a.gap_md]}>
+        <UserAvatar type="algo" size={48} avatar={info.avatar} />
+
+        <View style={[a.flex_1, a.gap_2xs]}>
+          <Text
+            style={[a.text_2xl, a.font_heavy, a.leading_tight]}
+            numberOfLines={2}>
+            {info.displayName}
+          </Text>
+          <Text
+            style={[a.text_sm, a.leading_tight, t.atoms.text_contrast_medium]}
+            numberOfLines={1}>
+            <Trans>
+              By{' '}
+              <InlineLinkText
+                label={_(msg`View ${info.creatorHandle}'s profile`)}
+                to={makeProfileLink({
+                  did: info.creatorDid,
+                  handle: info.creatorHandle,
+                })}
+                style={[
+                  a.text_sm,
+                  a.leading_tight,
+                  a.underline,
+                  t.atoms.text_contrast_medium,
+                ]}
+                numberOfLines={1}
+                onPress={() => control.close()}>
+                {sanitizeHandle(info.creatorHandle, '@')}
+              </InlineLinkText>
+            </Trans>
+          </Text>
+        </View>
+
+        <Button
+          label={_(msg`Share this feed`)}
+          size="small"
+          variant="ghost"
+          color="secondary"
+          shape="round"
+          onPress={onPressShare}>
+          <ButtonIcon icon={Share} size="lg" />
+        </Button>
+      </View>
+
+      <RichText value={rt} style={[a.text_md, a.leading_snug]} />
+
+      <View style={[a.flex_row, a.gap_sm, a.align_center]}>
+        {typeof likeCount === 'number' && (
+          <InlineLinkText
+            label={_(msg`View users who like this feed`)}
+            to={makeCustomFeedLink(info.creatorDid, feedRkey, 'liked-by')}
+            style={[a.underline, t.atoms.text_contrast_medium]}
+            onPress={() => control.close()}>
+            <Trans>
+              Liked by <Plural value={likeCount} one="# user" other="# users" />
+            </Trans>
+          </InlineLinkText>
+        )}
+      </View>
+
+      {hasSession && (
+        <>
+          <View style={[a.flex_row, a.gap_sm, a.align_center, a.pt_sm]}>
+            <Button
+              disabled={isLikePending || isUnlikePending}
+              label={_(msg`Like feed`)}
+              size="small"
+              variant="solid"
+              color="secondary"
+              onPress={onToggleLiked}
+              style={[a.flex_1]}>
+              {isLiked ? (
+                <HeartFilled size="sm" fill={t.palette.like} />
+              ) : (
+                <ButtonIcon icon={Heart} position="left" />
+              )}
+
+              <ButtonText>
+                {isLiked ? <Trans>Unlike</Trans> : <Trans>Like</Trans>}
+              </ButtonText>
+            </Button>
+            <Button
+              disabled={isFeedStateChangePending}
+              label={isPinned ? _(msg`Unpin feed`) : _(msg`Pin feed`)}
+              size="small"
+              variant="solid"
+              color={isPinned ? 'secondary' : 'primary'}
+              onPress={onTogglePinned}
+              style={[a.flex_1]}>
+              <ButtonText>
+                {isPinned ? <Trans>Unpin feed</Trans> : <Trans>Pin feed</Trans>}
+              </ButtonText>
+              <ButtonIcon icon={Pin} position="right" />
+            </Button>
+          </View>
+
+          <View style={[a.pt_xs, a.gap_lg]}>
+            <Divider />
+
+            <View
+              style={[a.flex_row, a.align_center, a.gap_sm, a.justify_between]}>
+              <Text style={[a.italic, t.atoms.text_contrast_medium]}>
+                Something wrong? Let us know.
+              </Text>
+
+              <Button
+                label={_(msg`Report feed`)}
+                size="small"
+                variant="solid"
+                color="secondary"
+                onPress={onPressReport}>
+                <ButtonText>
+                  <Trans>Report feed</Trans>
+                </ButtonText>
+                <ButtonIcon icon={CircleInfo} position="right" />
+              </Button>
+            </View>
+
+            <ReportDialog
+              control={reportDialogControl}
+              params={{
+                type: 'feedgen',
+                uri: info.uri,
+                cid: info.cid,
+              }}
+            />
+          </View>
+        </>
+      )}
+    </View>
+  )
+}
diff --git a/src/view/screens/ProfileFeed.tsx b/src/view/screens/ProfileFeed.tsx
deleted file mode 100644
index c3f98c067..000000000
--- a/src/view/screens/ProfileFeed.tsx
+++ /dev/null
@@ -1,621 +0,0 @@
-import React, {useCallback, useMemo} from 'react'
-import {Pressable, StyleSheet, View} from 'react-native'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {msg, Plural, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {useIsFocused, useNavigation} from '@react-navigation/native'
-import {NativeStackScreenProps} from '@react-navigation/native-stack'
-import {useQueryClient} from '@tanstack/react-query'
-
-import {HITSLOP_20} from '#/lib/constants'
-import {useHaptics} from '#/lib/haptics'
-import {usePalette} from '#/lib/hooks/usePalette'
-import {useSetTitle} from '#/lib/hooks/useSetTitle'
-import {ComposeIcon2} from '#/lib/icons'
-import {makeCustomFeedLink} from '#/lib/routes/links'
-import {CommonNavigatorParams} from '#/lib/routes/types'
-import {NavigationProp} from '#/lib/routes/types'
-import {shareUrl} from '#/lib/sharing'
-import {makeRecordUri} from '#/lib/strings/url-helpers'
-import {toShareUrl} from '#/lib/strings/url-helpers'
-import {s} from '#/lib/styles'
-import {logger} from '#/logger'
-import {isNative} from '#/platform/detection'
-import {listenSoftReset} from '#/state/events'
-import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback'
-import {FeedSourceFeedInfo, useFeedSourceInfoQuery} from '#/state/queries/feed'
-import {useLikeMutation, useUnlikeMutation} from '#/state/queries/like'
-import {FeedDescriptor} from '#/state/queries/post-feed'
-import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
-import {
-  useAddSavedFeedsMutation,
-  usePreferencesQuery,
-  UsePreferencesQueryResponse,
-  useRemoveFeedMutation,
-  useUpdateSavedFeedsMutation,
-} from '#/state/queries/preferences'
-import {useResolveUriQuery} from '#/state/queries/resolve-uri'
-import {truncateAndInvalidate} from '#/state/queries/util'
-import {useSession} from '#/state/session'
-import {useComposerControls} from '#/state/shell/composer'
-import {PagerWithHeader} from '#/view/com/pager/PagerWithHeader'
-import {PostFeed} from '#/view/com/posts/PostFeed'
-import {ProfileSubpageHeader} from '#/view/com/profile/ProfileSubpageHeader'
-import {EmptyState} from '#/view/com/util/EmptyState'
-import {FAB} from '#/view/com/util/fab/FAB'
-import {Button} from '#/view/com/util/forms/Button'
-import {ListRef} from '#/view/com/util/List'
-import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn'
-import {LoadingScreen} from '#/view/com/util/LoadingScreen'
-import {Text} from '#/view/com/util/text/Text'
-import * as Toast from '#/view/com/util/Toast'
-import {atoms as a, useTheme} from '#/alf'
-import {Button as NewButton, ButtonText} from '#/components/Button'
-import {useRichText} from '#/components/hooks/useRichText'
-import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox'
-import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
-import {
-  Heart2_Filled_Stroke2_Corner0_Rounded as HeartFilled,
-  Heart2_Stroke2_Corner0_Rounded as HeartOutline,
-} from '#/components/icons/Heart2'
-import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
-import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
-import * as Layout from '#/components/Layout'
-import {InlineLinkText} from '#/components/Link'
-import * as Menu from '#/components/Menu'
-import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog'
-import {RichText} from '#/components/RichText'
-
-const SECTION_TITLES = ['Posts']
-
-interface SectionRef {
-  scrollToTop: () => void
-}
-
-type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFeed'>
-export function ProfileFeedScreen(props: Props) {
-  const {rkey, name: handleOrDid} = props.route.params
-
-  const pal = usePalette('default')
-  const {_} = useLingui()
-  const navigation = useNavigation<NavigationProp>()
-
-  const uri = useMemo(
-    () => makeRecordUri(handleOrDid, 'app.bsky.feed.generator', rkey),
-    [rkey, handleOrDid],
-  )
-  const {error, data: resolvedUri} = useResolveUriQuery(uri)
-
-  const onPressBack = React.useCallback(() => {
-    if (navigation.canGoBack()) {
-      navigation.goBack()
-    } else {
-      navigation.navigate('Home')
-    }
-  }, [navigation])
-
-  if (error) {
-    return (
-      <Layout.Screen testID="profileFeedScreenError">
-        <Layout.Content>
-          <View style={[pal.view, pal.border, styles.notFoundContainer]}>
-            <Text type="title-lg" style={[pal.text, s.mb10]}>
-              <Trans>Could not load feed</Trans>
-            </Text>
-            <Text type="md" style={[pal.text, s.mb20]}>
-              {error.toString()}
-            </Text>
-
-            <View style={{flexDirection: 'row'}}>
-              <Button
-                type="default"
-                accessibilityLabel={_(msg`Go back`)}
-                accessibilityHint={_(msg`Returns to previous page`)}
-                onPress={onPressBack}
-                style={{flexShrink: 1}}>
-                <Text type="button" style={pal.text}>
-                  <Trans>Go Back</Trans>
-                </Text>
-              </Button>
-            </View>
-          </View>
-        </Layout.Content>
-      </Layout.Screen>
-    )
-  }
-
-  return resolvedUri ? (
-    <Layout.Screen>
-      <ProfileFeedScreenIntermediate feedUri={resolvedUri.uri} />
-    </Layout.Screen>
-  ) : (
-    <Layout.Screen>
-      <LoadingScreen />
-    </Layout.Screen>
-  )
-}
-
-function ProfileFeedScreenIntermediate({feedUri}: {feedUri: string}) {
-  const {data: preferences} = usePreferencesQuery()
-  const {data: info} = useFeedSourceInfoQuery({uri: feedUri})
-
-  if (!preferences || !info) {
-    return <LoadingScreen />
-  }
-
-  return (
-    <ProfileFeedScreenInner
-      preferences={preferences}
-      feedInfo={info as FeedSourceFeedInfo}
-    />
-  )
-}
-
-export function ProfileFeedScreenInner({
-  preferences,
-  feedInfo,
-}: {
-  preferences: UsePreferencesQueryResponse
-  feedInfo: FeedSourceFeedInfo
-}) {
-  const {_} = useLingui()
-  const t = useTheme()
-  const {hasSession, currentAccount} = useSession()
-  const reportDialogControl = useReportDialogControl()
-  const {openComposer} = useComposerControls()
-  const playHaptic = useHaptics()
-  const feedSectionRef = React.useRef<SectionRef>(null)
-  const isScreenFocused = useIsFocused()
-
-  const {mutateAsync: addSavedFeeds, isPending: isAddSavedFeedPending} =
-    useAddSavedFeedsMutation()
-  const {mutateAsync: removeFeed, isPending: isRemovePending} =
-    useRemoveFeedMutation()
-  const {mutateAsync: updateSavedFeeds, isPending: isUpdateFeedPending} =
-    useUpdateSavedFeedsMutation()
-
-  const isPending =
-    isAddSavedFeedPending || isRemovePending || isUpdateFeedPending
-  const savedFeedConfig = preferences.savedFeeds.find(
-    f => f.value === feedInfo.uri,
-  )
-  const isSaved = Boolean(savedFeedConfig)
-  const isPinned = Boolean(savedFeedConfig?.pinned)
-
-  useSetTitle(feedInfo?.displayName)
-
-  // event handlers
-  //
-
-  const onToggleSaved = React.useCallback(async () => {
-    try {
-      playHaptic()
-
-      if (savedFeedConfig) {
-        await removeFeed(savedFeedConfig)
-        Toast.show(_(msg`Removed from your feeds`))
-      } else {
-        await addSavedFeeds([
-          {
-            type: 'feed',
-            value: feedInfo.uri,
-            pinned: false,
-          },
-        ])
-        Toast.show(_(msg`Saved to your feeds`))
-      }
-    } catch (err) {
-      Toast.show(
-        _(
-          msg`There was an issue updating your feeds, please check your internet connection and try again.`,
-        ),
-        'xmark',
-      )
-      logger.error('Failed to update feeds', {message: err})
-    }
-  }, [_, playHaptic, feedInfo, removeFeed, addSavedFeeds, savedFeedConfig])
-
-  const onTogglePinned = React.useCallback(async () => {
-    try {
-      playHaptic()
-
-      if (savedFeedConfig) {
-        await updateSavedFeeds([
-          {
-            ...savedFeedConfig,
-            pinned: !savedFeedConfig.pinned,
-          },
-        ])
-      } else {
-        await addSavedFeeds([
-          {
-            type: 'feed',
-            value: feedInfo.uri,
-            pinned: true,
-          },
-        ])
-      }
-    } catch (e) {
-      Toast.show(_(msg`There was an issue contacting the server`), 'xmark')
-      logger.error('Failed to toggle pinned feed', {message: e})
-    }
-  }, [
-    playHaptic,
-    feedInfo,
-    _,
-    savedFeedConfig,
-    updateSavedFeeds,
-    addSavedFeeds,
-  ])
-
-  const onPressShare = React.useCallback(() => {
-    const url = toShareUrl(feedInfo.route.href)
-    shareUrl(url)
-  }, [feedInfo])
-
-  const onPressReport = React.useCallback(() => {
-    reportDialogControl.open()
-  }, [reportDialogControl])
-
-  const onCurrentPageSelected = React.useCallback(
-    (index: number) => {
-      if (index === 0) {
-        feedSectionRef.current?.scrollToTop()
-      }
-    },
-    [feedSectionRef],
-  )
-
-  const renderHeader = useCallback(() => {
-    return (
-      <>
-        <ProfileSubpageHeader
-          isLoading={false}
-          href={feedInfo.route.href}
-          title={feedInfo?.displayName}
-          avatar={feedInfo?.avatar}
-          isOwner={feedInfo.creatorDid === currentAccount?.did}
-          creator={
-            feedInfo
-              ? {did: feedInfo.creatorDid, handle: feedInfo.creatorHandle}
-              : undefined
-          }
-          avatarType="algo">
-          <View style={[a.flex_row, a.align_center, a.gap_sm]}>
-            {feedInfo && hasSession && (
-              <NewButton
-                testID={isPinned ? 'unpinBtn' : 'pinBtn'}
-                disabled={isPending}
-                size="small"
-                variant="solid"
-                color={isPinned ? 'secondary' : 'primary'}
-                label={isPinned ? _(msg`Unpin from home`) : _(msg`Pin to home`)}
-                onPress={onTogglePinned}>
-                <ButtonText>
-                  {isPinned ? _(msg`Unpin`) : _(msg`Pin to Home`)}
-                </ButtonText>
-              </NewButton>
-            )}
-            <Menu.Root>
-              <Menu.Trigger label={_(msg`Open feed options menu`)}>
-                {({props, state}) => {
-                  return (
-                    <Pressable
-                      {...props}
-                      hitSlop={HITSLOP_20}
-                      style={[
-                        a.justify_center,
-                        a.align_center,
-                        a.rounded_full,
-                        {height: 36, width: 36},
-                        t.atoms.bg_contrast_25,
-                        (state.hovered || state.pressed) && [
-                          t.atoms.bg_contrast_50,
-                        ],
-                      ]}
-                      testID="headerDropdownBtn">
-                      <FontAwesomeIcon
-                        icon="ellipsis"
-                        size={20}
-                        style={t.atoms.text}
-                      />
-                    </Pressable>
-                  )
-                }}
-              </Menu.Trigger>
-
-              <Menu.Outer>
-                <Menu.Group>
-                  {hasSession && (
-                    <>
-                      <Menu.Item
-                        disabled={isPending}
-                        testID="feedHeaderDropdownToggleSavedBtn"
-                        label={
-                          isSaved
-                            ? _(msg`Remove from my feeds`)
-                            : _(msg`Save to my feeds`)
-                        }
-                        onPress={onToggleSaved}>
-                        <Menu.ItemText>
-                          {isSaved
-                            ? _(msg`Remove from my feeds`)
-                            : _(msg`Save to my feeds`)}
-                        </Menu.ItemText>
-                        <Menu.ItemIcon
-                          icon={isSaved ? Trash : Plus}
-                          position="right"
-                        />
-                      </Menu.Item>
-
-                      <Menu.Item
-                        testID="feedHeaderDropdownReportBtn"
-                        label={_(msg`Report feed`)}
-                        onPress={onPressReport}>
-                        <Menu.ItemText>{_(msg`Report feed`)}</Menu.ItemText>
-                        <Menu.ItemIcon icon={CircleInfo} position="right" />
-                      </Menu.Item>
-                    </>
-                  )}
-
-                  <Menu.Item
-                    testID="feedHeaderDropdownShareBtn"
-                    label={_(msg`Share feed`)}
-                    onPress={onPressShare}>
-                    <Menu.ItemText>{_(msg`Share feed`)}</Menu.ItemText>
-                    <Menu.ItemIcon icon={Share} position="right" />
-                  </Menu.Item>
-                </Menu.Group>
-              </Menu.Outer>
-            </Menu.Root>
-          </View>
-        </ProfileSubpageHeader>
-        <AboutSection
-          feedOwnerDid={feedInfo.creatorDid}
-          feedRkey={feedInfo.route.params.rkey}
-          feedInfo={feedInfo}
-        />
-      </>
-    )
-  }, [
-    _,
-    hasSession,
-    feedInfo,
-    isPinned,
-    onTogglePinned,
-    onToggleSaved,
-    currentAccount?.did,
-    isSaved,
-    onPressReport,
-    onPressShare,
-    t,
-    isPending,
-  ])
-
-  return (
-    <>
-      <ReportDialog
-        control={reportDialogControl}
-        params={{
-          type: 'feedgen',
-          uri: feedInfo.uri,
-          cid: feedInfo.cid,
-        }}
-      />
-      <PagerWithHeader
-        items={SECTION_TITLES}
-        isHeaderReady={true}
-        renderHeader={renderHeader}
-        onCurrentPageSelected={onCurrentPageSelected}>
-        {({headerHeight, scrollElRef, isFocused}) => (
-          <FeedSection
-            ref={feedSectionRef}
-            feed={`feedgen|${feedInfo.uri}`}
-            headerHeight={headerHeight}
-            scrollElRef={scrollElRef as ListRef}
-            isFocused={isScreenFocused && isFocused}
-          />
-        )}
-      </PagerWithHeader>
-      {hasSession && (
-        <FAB
-          testID="composeFAB"
-          onPress={() => openComposer({})}
-          icon={
-            <ComposeIcon2
-              strokeWidth={1.5}
-              size={29}
-              style={{color: 'white'}}
-            />
-          }
-          accessibilityRole="button"
-          accessibilityLabel={_(msg`New post`)}
-          accessibilityHint=""
-        />
-      )}
-    </>
-  )
-}
-
-interface FeedSectionProps {
-  feed: FeedDescriptor
-  headerHeight: number
-  scrollElRef: ListRef
-  isFocused: boolean
-}
-const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
-  function FeedSectionImpl({feed, headerHeight, scrollElRef, isFocused}, ref) {
-    const {_} = useLingui()
-    const [hasNew, setHasNew] = React.useState(false)
-    const [isScrolledDown, setIsScrolledDown] = React.useState(false)
-    const queryClient = useQueryClient()
-    const isScreenFocused = useIsFocused()
-    const {hasSession} = useSession()
-    const feedFeedback = useFeedFeedback(feed, hasSession)
-
-    const onScrollToTop = useCallback(() => {
-      scrollElRef.current?.scrollToOffset({
-        animated: isNative,
-        offset: -headerHeight,
-      })
-      truncateAndInvalidate(queryClient, FEED_RQKEY(feed))
-      setHasNew(false)
-    }, [scrollElRef, headerHeight, queryClient, feed, setHasNew])
-
-    React.useImperativeHandle(ref, () => ({
-      scrollToTop: onScrollToTop,
-    }))
-
-    React.useEffect(() => {
-      if (!isScreenFocused) {
-        return
-      }
-      return listenSoftReset(onScrollToTop)
-    }, [onScrollToTop, isScreenFocused])
-
-    const renderPostsEmpty = useCallback(() => {
-      return <EmptyState icon="hashtag" message={_(msg`This feed is empty.`)} />
-    }, [_])
-
-    return (
-      <View>
-        <FeedFeedbackProvider value={feedFeedback}>
-          <PostFeed
-            enabled={isFocused}
-            feed={feed}
-            pollInterval={60e3}
-            disablePoll={hasNew}
-            scrollElRef={scrollElRef}
-            onHasNew={setHasNew}
-            onScrolledDownChange={setIsScrolledDown}
-            renderEmptyState={renderPostsEmpty}
-            headerOffset={headerHeight}
-          />
-        </FeedFeedbackProvider>
-        {(isScrolledDown || hasNew) && (
-          <LoadLatestBtn
-            onPress={onScrollToTop}
-            label={_(msg`Load new posts`)}
-            showIndicator={hasNew}
-          />
-        )}
-      </View>
-    )
-  },
-)
-
-function AboutSection({
-  feedOwnerDid,
-  feedRkey,
-  feedInfo,
-}: {
-  feedOwnerDid: string
-  feedRkey: string
-  feedInfo: FeedSourceFeedInfo
-}) {
-  const t = useTheme()
-  const pal = usePalette('default')
-  const {_} = useLingui()
-  const [likeUri, setLikeUri] = React.useState(feedInfo.likeUri)
-  const {hasSession} = useSession()
-  const playHaptic = useHaptics()
-  const {mutateAsync: likeFeed, isPending: isLikePending} = useLikeMutation()
-  const {mutateAsync: unlikeFeed, isPending: isUnlikePending} =
-    useUnlikeMutation()
-  const [resolvedRT] = useRichText(feedInfo.description.text || '')
-
-  const isLiked = !!likeUri
-  const likeCount =
-    isLiked && likeUri ? (feedInfo.likeCount || 0) + 1 : feedInfo.likeCount
-
-  const onToggleLiked = React.useCallback(async () => {
-    try {
-      playHaptic()
-
-      if (isLiked && likeUri) {
-        await unlikeFeed({uri: likeUri})
-        setLikeUri('')
-      } else {
-        const res = await likeFeed({uri: feedInfo.uri, cid: feedInfo.cid})
-        setLikeUri(res.uri)
-      }
-    } catch (err) {
-      Toast.show(
-        _(
-          msg`There was an issue contacting the server, please check your internet connection and try again.`,
-        ),
-        'xmark',
-      )
-      logger.error('Failed to toggle like', {message: err})
-    }
-  }, [playHaptic, isLiked, likeUri, unlikeFeed, likeFeed, feedInfo, _])
-
-  return (
-    <View style={[styles.aboutSectionContainer]}>
-      <View style={[a.pt_sm]}>
-        {feedInfo.description ? (
-          <RichText
-            testID="listDescription"
-            style={[a.text_md]}
-            value={resolvedRT ?? feedInfo.description}
-          />
-        ) : (
-          <Text type="lg" style={[{fontStyle: 'italic'}, pal.textLight]}>
-            <Trans>No description</Trans>
-          </Text>
-        )}
-      </View>
-
-      <View style={[a.flex_row, a.gap_sm, a.align_center, a.pb_sm]}>
-        <NewButton
-          size="small"
-          variant="solid"
-          color="secondary"
-          shape="round"
-          label={isLiked ? _(msg`Unlike this feed`) : _(msg`Like this feed`)}
-          testID="toggleLikeBtn"
-          disabled={!hasSession || isLikePending || isUnlikePending}
-          onPress={onToggleLiked}>
-          {isLiked ? (
-            <HeartFilled size="md" fill={s.likeColor.color} />
-          ) : (
-            <HeartOutline size="md" fill={t.atoms.text_contrast_medium.color} />
-          )}
-        </NewButton>
-        {typeof likeCount === 'number' && (
-          <InlineLinkText
-            label={_(msg`View users who like this feed`)}
-            to={makeCustomFeedLink(feedOwnerDid, feedRkey, 'liked-by')}
-            style={[t.atoms.text_contrast_medium, a.font_bold]}>
-            <Trans>
-              Liked by <Plural value={likeCount} one="# user" other="# users" />
-            </Trans>
-          </InlineLinkText>
-        )}
-      </View>
-    </View>
-  )
-}
-
-const styles = StyleSheet.create({
-  btn: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    gap: 6,
-    paddingVertical: 7,
-    paddingHorizontal: 14,
-    borderRadius: 50,
-    marginLeft: 6,
-  },
-  notFoundContainer: {
-    margin: 10,
-    paddingHorizontal: 18,
-    paddingVertical: 14,
-    borderRadius: 6,
-  },
-  aboutSectionContainer: {
-    paddingVertical: 4,
-    paddingHorizontal: 16,
-    gap: 12,
-  },
-})