about summary refs log tree commit diff
path: root/src/view/screens/ProfileFeed.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/screens/ProfileFeed.tsx')
-rw-r--r--src/view/screens/ProfileFeed.tsx780
1 files changed, 461 insertions, 319 deletions
diff --git a/src/view/screens/ProfileFeed.tsx b/src/view/screens/ProfileFeed.tsx
index a4d146d66..3a0bdcc0f 100644
--- a/src/view/screens/ProfileFeed.tsx
+++ b/src/view/screens/ProfileFeed.tsx
@@ -1,25 +1,21 @@
 import React, {useMemo, useCallback} from 'react'
 import {
-  FlatList,
-  NativeScrollEvent,
+  Dimensions,
   StyleSheet,
   View,
   ActivityIndicator,
+  FlatList,
 } from 'react-native'
 import {NativeStackScreenProps} from '@react-navigation/native-stack'
 import {useNavigation} from '@react-navigation/native'
-import {useAnimatedScrollHandler} from 'react-native-reanimated'
+import {useQueryClient} from '@tanstack/react-query'
 import {usePalette} from 'lib/hooks/usePalette'
 import {HeartIcon, HeartIconSolid} from 'lib/icons'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {CommonNavigatorParams} from 'lib/routes/types'
 import {makeRecordUri} from 'lib/strings/url-helpers'
 import {colors, s} from 'lib/styles'
-import {observer} from 'mobx-react-lite'
-import {useStores} from 'state/index'
-import {FeedSourceModel} from 'state/models/content/feed-source'
-import {PostsFeedModel} from 'state/models/feeds/posts'
-import {withAuthRequired} from 'view/com/auth/withAuthRequired'
+import {FeedDescriptor} from '#/state/queries/post-feed'
 import {PagerWithHeader} from 'view/com/pager/PagerWithHeader'
 import {ProfileSubpageHeader} from 'view/com/profile/ProfileSubpageHeader'
 import {Feed} from 'view/com/posts/Feed'
@@ -32,13 +28,13 @@ import {FAB} from 'view/com/util/fab/FAB'
 import {EmptyState} from 'view/com/util/EmptyState'
 import * as Toast from 'view/com/util/Toast'
 import {useSetTitle} from 'lib/hooks/useSetTitle'
-import {useCustomFeed} from 'lib/hooks/useCustomFeed'
+import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
+import {OnScrollHandler} from 'lib/hooks/useOnMainScroll'
 import {shareUrl} from 'lib/sharing'
 import {toShareUrl} from 'lib/strings/url-helpers'
 import {Haptics} from 'lib/haptics'
 import {useAnalytics} from 'lib/analytics/analytics'
 import {NativeDropdown, DropdownItem} from 'view/com/util/forms/NativeDropdown'
-import {resolveName} from 'lib/api'
 import {makeCustomFeedLink} from 'lib/routes/links'
 import {pluralize} from 'lib/strings/helpers'
 import {CenteredView, ScrollView} from 'view/com/util/Views'
@@ -47,6 +43,28 @@ import {sanitizeHandle} from 'lib/strings/handles'
 import {makeProfileLink} from 'lib/routes/links'
 import {ComposeIcon2} from 'lib/icons'
 import {logger} from '#/logger'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useModalControls} from '#/state/modals'
+import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
+import {
+  useFeedSourceInfoQuery,
+  FeedSourceFeedInfo,
+  useIsFeedPublicQuery,
+} from '#/state/queries/feed'
+import {useResolveUriQuery} from '#/state/queries/resolve-uri'
+import {
+  UsePreferencesQueryResponse,
+  usePreferencesQuery,
+  useSaveFeedMutation,
+  useRemoveFeedMutation,
+  usePinFeedMutation,
+  useUnpinFeedMutation,
+} from '#/state/queries/preferences'
+import {useSession} from '#/state/session'
+import {useLikeMutation, useUnlikeMutation} from '#/state/queries/like'
+import {useComposerControls} from '#/state/shell/composer'
+import {truncateAndInvalidate} from '#/state/queries/util'
 
 const SECTION_TITLES = ['Posts', 'About']
 
@@ -55,315 +73,372 @@ interface SectionRef {
 }
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFeed'>
-export const ProfileFeedScreen = withAuthRequired(
-  observer(function ProfileFeedScreenImpl(props: Props) {
-    const pal = usePalette('default')
-    const store = useStores()
-    const navigation = useNavigation<NavigationProp>()
+export function ProfileFeedScreen(props: Props) {
+  const {rkey, name: handleOrDid} = props.route.params
 
-    const {name: handleOrDid} = props.route.params
+  const pal = usePalette('default')
+  const {_} = useLingui()
+  const navigation = useNavigation<NavigationProp>()
 
-    const [feedOwnerDid, setFeedOwnerDid] = React.useState<string | undefined>()
-    const [error, setError] = React.useState<string | undefined>()
+  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])
-
-    React.useEffect(() => {
-      /*
-       * We must resolve the DID of the feed owner before we can fetch the feed.
-       */
-      async function fetchDid() {
-        try {
-          const did = await resolveName(store, handleOrDid)
-          setFeedOwnerDid(did)
-        } catch (e) {
-          setError(
-            `We're sorry, but we were unable to resolve this feed. If this persists, please contact the feed creator, @${handleOrDid}.`,
-          )
-        }
-      }
+  const onPressBack = React.useCallback(() => {
+    if (navigation.canGoBack()) {
+      navigation.goBack()
+    } else {
+      navigation.navigate('Home')
+    }
+  }, [navigation])
+
+  if (error) {
+    return (
+      <CenteredView>
+        <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>
 
-      fetchDid()
-    }, [store, handleOrDid, setFeedOwnerDid])
-
-    if (error) {
-      return (
-        <CenteredView>
-          <View style={[pal.view, pal.border, styles.notFoundContainer]}>
-            <Text type="title-lg" style={[pal.text, s.mb10]}>
-              Could not load feed
-            </Text>
-            <Text type="md" style={[pal.text, s.mb20]}>
-              {error}
-            </Text>
-
-            <View style={{flexDirection: 'row'}}>
-              <Button
-                type="default"
-                accessibilityLabel="Go Back"
-                accessibilityHint="Return to previous page"
-                onPress={onPressBack}
-                style={{flexShrink: 1}}>
-                <Text type="button" style={pal.text}>
-                  Go Back
-                </Text>
-              </Button>
-            </View>
+          <View style={{flexDirection: 'row'}}>
+            <Button
+              type="default"
+              accessibilityLabel={_(msg`Go Back`)}
+              accessibilityHint="Return to previous page"
+              onPress={onPressBack}
+              style={{flexShrink: 1}}>
+              <Text type="button" style={pal.text}>
+                <Trans>Go Back</Trans>
+              </Text>
+            </Button>
           </View>
-        </CenteredView>
-      )
-    }
+        </View>
+      </CenteredView>
+    )
+  }
 
-    return feedOwnerDid ? (
-      <ProfileFeedScreenInner {...props} feedOwnerDid={feedOwnerDid} />
-    ) : (
+  return resolvedUri ? (
+    <ProfileFeedScreenIntermediate feedUri={resolvedUri.uri} />
+  ) : (
+    <CenteredView>
+      <View style={s.p20}>
+        <ActivityIndicator size="large" />
+      </View>
+    </CenteredView>
+  )
+}
+
+function ProfileFeedScreenIntermediate({feedUri}: {feedUri: string}) {
+  const {data: preferences} = usePreferencesQuery()
+  const {data: info} = useFeedSourceInfoQuery({uri: feedUri})
+  const {isLoading: isPublicStatusLoading, data: isPublic} =
+    useIsFeedPublicQuery({uri: feedUri})
+
+  if (!preferences || !info || isPublicStatusLoading) {
+    return (
       <CenteredView>
         <View style={s.p20}>
           <ActivityIndicator size="large" />
         </View>
       </CenteredView>
     )
-  }),
-)
+  }
 
-export const ProfileFeedScreenInner = observer(
-  function ProfileFeedScreenInnerImpl({
-    route,
-    feedOwnerDid,
-  }: Props & {feedOwnerDid: string}) {
-    const pal = usePalette('default')
-    const store = useStores()
-    const {track} = useAnalytics()
-    const feedSectionRef = React.useRef<SectionRef>(null)
-    const {rkey, name: handleOrDid} = route.params
-    const uri = useMemo(
-      () => makeRecordUri(feedOwnerDid, 'app.bsky.feed.generator', rkey),
-      [rkey, feedOwnerDid],
-    )
-    const feedInfo = useCustomFeed(uri)
-    const feed: PostsFeedModel = useMemo(() => {
-      const model = new PostsFeedModel(store, 'custom', {
-        feed: uri,
-      })
-      model.setup()
-      return model
-    }, [store, uri])
-    const isPinned = store.preferences.isPinnedFeed(uri)
-    useSetTitle(feedInfo?.displayName)
-
-    // events
-    // =
-
-    const onToggleSaved = React.useCallback(async () => {
-      try {
-        Haptics.default()
-        if (feedInfo?.isSaved) {
-          await feedInfo?.unsave()
-        } else {
-          await feedInfo?.save()
-        }
-      } catch (err) {
-        Toast.show(
-          'There was an an issue updating your feeds, please check your internet connection and try again.',
-        )
-        logger.error('Failed up update feeds', {error: err})
-      }
-    }, [feedInfo])
+  return (
+    <ProfileFeedScreenInner
+      preferences={preferences}
+      feedInfo={info as FeedSourceFeedInfo}
+      isPublic={Boolean(isPublic)}
+    />
+  )
+}
 
-    const onToggleLiked = React.useCallback(async () => {
+export function ProfileFeedScreenInner({
+  preferences,
+  feedInfo,
+  isPublic,
+}: {
+  preferences: UsePreferencesQueryResponse
+  feedInfo: FeedSourceFeedInfo
+  isPublic: boolean
+}) {
+  const {_} = useLingui()
+  const pal = usePalette('default')
+  const {hasSession, currentAccount} = useSession()
+  const {openModal} = useModalControls()
+  const {openComposer} = useComposerControls()
+  const {track} = useAnalytics()
+  const feedSectionRef = React.useRef<SectionRef>(null)
+
+  const {
+    mutateAsync: saveFeed,
+    variables: savedFeed,
+    reset: resetSaveFeed,
+    isPending: isSavePending,
+  } = useSaveFeedMutation()
+  const {
+    mutateAsync: removeFeed,
+    variables: removedFeed,
+    reset: resetRemoveFeed,
+    isPending: isRemovePending,
+  } = useRemoveFeedMutation()
+  const {
+    mutateAsync: pinFeed,
+    variables: pinnedFeed,
+    reset: resetPinFeed,
+    isPending: isPinPending,
+  } = usePinFeedMutation()
+  const {
+    mutateAsync: unpinFeed,
+    variables: unpinnedFeed,
+    reset: resetUnpinFeed,
+    isPending: isUnpinPending,
+  } = useUnpinFeedMutation()
+
+  const isSaved =
+    !removedFeed &&
+    (!!savedFeed || preferences.feeds.saved.includes(feedInfo.uri))
+  const isPinned =
+    !unpinnedFeed &&
+    (!!pinnedFeed || preferences.feeds.pinned.includes(feedInfo.uri))
+
+  useSetTitle(feedInfo?.displayName)
+
+  const onToggleSaved = React.useCallback(async () => {
+    try {
       Haptics.default()
-      try {
-        if (feedInfo?.isLiked) {
-          await feedInfo?.unlike()
-        } else {
-          await feedInfo?.like()
-        }
-      } catch (err) {
-        Toast.show(
-          'There was an an issue contacting the server, please check your internet connection and try again.',
-        )
-        logger.error('Failed up toggle like', {error: err})
+
+      if (isSaved) {
+        await removeFeed({uri: feedInfo.uri})
+        resetRemoveFeed()
+      } else {
+        await saveFeed({uri: feedInfo.uri})
+        resetSaveFeed()
       }
-    }, [feedInfo])
+    } catch (err) {
+      Toast.show(
+        'There was an an issue updating your feeds, please check your internet connection and try again.',
+      )
+      logger.error('Failed up update feeds', {error: err})
+    }
+  }, [feedInfo, isSaved, saveFeed, removeFeed, resetSaveFeed, resetRemoveFeed])
 
-    const onTogglePinned = React.useCallback(async () => {
+  const onTogglePinned = React.useCallback(async () => {
+    try {
       Haptics.default()
-      if (feedInfo) {
-        feedInfo.togglePin().catch(e => {
-          Toast.show('There was an issue contacting the server')
-          logger.error('Failed to toggle pinned feed', {error: e})
-        })
-      }
-    }, [feedInfo])
-
-    const onPressShare = React.useCallback(() => {
-      const url = toShareUrl(`/profile/${handleOrDid}/feed/${rkey}`)
-      shareUrl(url)
-      track('CustomFeed:Share')
-    }, [handleOrDid, rkey, track])
-
-    const onPressReport = React.useCallback(() => {
-      if (!feedInfo) return
-      store.shell.openModal({
-        name: 'report',
-        uri: feedInfo.uri,
-        cid: feedInfo.cid,
-      })
-    }, [store, feedInfo])
-
-    const onCurrentPageSelected = React.useCallback(
-      (index: number) => {
-        if (index === 0) {
-          feedSectionRef.current?.scrollToTop()
-        }
-      },
-      [feedSectionRef],
-    )
 
-    // render
-    // =
+      if (isPinned) {
+        await unpinFeed({uri: feedInfo.uri})
+        resetUnpinFeed()
+      } else {
+        await pinFeed({uri: feedInfo.uri})
+        resetPinFeed()
+      }
+    } catch (e) {
+      Toast.show('There was an issue contacting the server')
+      logger.error('Failed to toggle pinned feed', {error: e})
+    }
+  }, [isPinned, feedInfo, pinFeed, unpinFeed, resetPinFeed, resetUnpinFeed])
+
+  const onPressShare = React.useCallback(() => {
+    const url = toShareUrl(feedInfo.route.href)
+    shareUrl(url)
+    track('CustomFeed:Share')
+  }, [feedInfo, track])
+
+  const onPressReport = React.useCallback(() => {
+    if (!feedInfo) return
+    openModal({
+      name: 'report',
+      uri: feedInfo.uri,
+      cid: feedInfo.cid,
+    })
+  }, [openModal, feedInfo])
+
+  const onCurrentPageSelected = React.useCallback(
+    (index: number) => {
+      if (index === 0) {
+        feedSectionRef.current?.scrollToTop()
+      }
+    },
+    [feedSectionRef],
+  )
 
-    const dropdownItems: DropdownItem[] = React.useMemo(() => {
-      return [
-        {
-          testID: 'feedHeaderDropdownToggleSavedBtn',
-          label: feedInfo?.isSaved ? 'Remove from my feeds' : 'Add to my feeds',
-          onPress: onToggleSaved,
-          icon: feedInfo?.isSaved
-            ? {
-                ios: {
-                  name: 'trash',
-                },
-                android: 'ic_delete',
-                web: ['far', 'trash-can'],
-              }
-            : {
-                ios: {
-                  name: 'plus',
-                },
-                android: '',
-                web: 'plus',
+  // render
+  // =
+
+  const dropdownItems: DropdownItem[] = React.useMemo(() => {
+    return [
+      hasSession && {
+        testID: 'feedHeaderDropdownToggleSavedBtn',
+        label: isSaved ? _(msg`Remove from my feeds`) : _(msg`Add to my feeds`),
+        onPress: isSavePending || isRemovePending ? undefined : onToggleSaved,
+        icon: isSaved
+          ? {
+              ios: {
+                name: 'trash',
               },
-        },
-        {
-          testID: 'feedHeaderDropdownReportBtn',
-          label: 'Report feed',
-          onPress: onPressReport,
-          icon: {
-            ios: {
-              name: 'exclamationmark.triangle',
+              android: 'ic_delete',
+              web: ['far', 'trash-can'],
+            }
+          : {
+              ios: {
+                name: 'plus',
+              },
+              android: '',
+              web: 'plus',
             },
-            android: 'ic_menu_report_image',
-            web: 'circle-exclamation',
+      },
+      hasSession && {
+        testID: 'feedHeaderDropdownReportBtn',
+        label: _(msg`Report feed`),
+        onPress: onPressReport,
+        icon: {
+          ios: {
+            name: 'exclamationmark.triangle',
           },
+          android: 'ic_menu_report_image',
+          web: 'circle-exclamation',
         },
-        {
-          testID: 'feedHeaderDropdownShareBtn',
-          label: 'Share link',
-          onPress: onPressShare,
-          icon: {
-            ios: {
-              name: 'square.and.arrow.up',
-            },
-            android: 'ic_menu_share',
-            web: 'share',
+      },
+      {
+        testID: 'feedHeaderDropdownShareBtn',
+        label: _(msg`Share feed`),
+        onPress: onPressShare,
+        icon: {
+          ios: {
+            name: 'square.and.arrow.up',
           },
+          android: 'ic_menu_share',
+          web: 'share',
         },
-      ] as DropdownItem[]
-    }, [feedInfo, onToggleSaved, onPressReport, onPressShare])
-
-    const renderHeader = useCallback(() => {
-      return (
-        <ProfileSubpageHeader
-          isLoading={!feedInfo?.hasLoaded}
-          href={makeCustomFeedLink(feedOwnerDid, rkey)}
-          title={feedInfo?.displayName}
-          avatar={feedInfo?.avatar}
-          isOwner={feedInfo?.isOwner}
-          creator={
-            feedInfo
-              ? {did: feedInfo.creatorDid, handle: feedInfo.creatorHandle}
-              : undefined
-          }
-          avatarType="algo">
-          {feedInfo && (
-            <>
-              <Button
-                type="default"
-                label={feedInfo?.isSaved ? 'Unsave' : 'Save'}
-                onPress={onToggleSaved}
-                style={styles.btn}
-              />
-              <Button
-                type={isPinned ? 'default' : 'inverted'}
-                label={isPinned ? 'Unpin' : 'Pin to home'}
-                onPress={onTogglePinned}
-                style={styles.btn}
-              />
-            </>
-          )}
-          <NativeDropdown
-            testID="headerDropdownBtn"
-            items={dropdownItems}
-            accessibilityLabel="More options"
-            accessibilityHint="">
-            <View style={[pal.viewLight, styles.btn]}>
-              <FontAwesomeIcon
-                icon="ellipsis"
-                size={20}
-                color={pal.colors.text}
-              />
-            </View>
-          </NativeDropdown>
-        </ProfileSubpageHeader>
-      )
-    }, [
-      pal,
-      feedOwnerDid,
-      rkey,
-      feedInfo,
-      isPinned,
-      onTogglePinned,
-      onToggleSaved,
-      dropdownItems,
-    ])
-
+      },
+    ].filter(Boolean) as DropdownItem[]
+  }, [
+    hasSession,
+    onToggleSaved,
+    onPressReport,
+    onPressShare,
+    isSaved,
+    isSavePending,
+    isRemovePending,
+    _,
+  ])
+
+  const renderHeader = useCallback(() => {
     return (
-      <View style={s.hContentRegion}>
-        <PagerWithHeader
-          items={SECTION_TITLES}
-          isHeaderReady={feedInfo?.hasLoaded ?? false}
-          renderHeader={renderHeader}
-          onCurrentPageSelected={onCurrentPageSelected}>
-          {({onScroll, headerHeight, isScrolledDown}) => (
+      <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">
+        {feedInfo && hasSession && (
+          <>
+            <Button
+              disabled={isSavePending || isRemovePending}
+              type="default"
+              label={isSaved ? 'Unsave' : 'Save'}
+              onPress={onToggleSaved}
+              style={styles.btn}
+            />
+            <Button
+              testID={isPinned ? 'unpinBtn' : 'pinBtn'}
+              disabled={isPinPending || isUnpinPending}
+              type={isPinned ? 'default' : 'inverted'}
+              label={isPinned ? 'Unpin' : 'Pin to home'}
+              onPress={onTogglePinned}
+              style={styles.btn}
+            />
+          </>
+        )}
+        <NativeDropdown
+          testID="headerDropdownBtn"
+          items={dropdownItems}
+          accessibilityLabel={_(msg`More options`)}
+          accessibilityHint="">
+          <View style={[pal.viewLight, styles.btn]}>
+            <FontAwesomeIcon
+              icon="ellipsis"
+              size={20}
+              color={pal.colors.text}
+            />
+          </View>
+        </NativeDropdown>
+      </ProfileSubpageHeader>
+    )
+  }, [
+    _,
+    hasSession,
+    pal,
+    feedInfo,
+    isPinned,
+    onTogglePinned,
+    onToggleSaved,
+    dropdownItems,
+    currentAccount?.did,
+    isPinPending,
+    isRemovePending,
+    isSavePending,
+    isSaved,
+    isUnpinPending,
+  ])
+
+  return (
+    <View style={s.hContentRegion}>
+      <PagerWithHeader
+        items={SECTION_TITLES}
+        isHeaderReady={true}
+        renderHeader={renderHeader}
+        onCurrentPageSelected={onCurrentPageSelected}>
+        {({onScroll, headerHeight, isScrolledDown, scrollElRef, isFocused}) =>
+          isPublic ? (
             <FeedSection
               ref={feedSectionRef}
-              feed={feed}
+              feed={`feedgen|${feedInfo.uri}`}
               onScroll={onScroll}
               headerHeight={headerHeight}
               isScrolledDown={isScrolledDown}
+              scrollElRef={
+                scrollElRef as React.MutableRefObject<FlatList<any> | null>
+              }
+              isFocused={isFocused}
             />
-          )}
-          {({onScroll, headerHeight}) => (
-            <AboutSection
-              feedOwnerDid={feedOwnerDid}
-              feedRkey={rkey}
-              feedInfo={feedInfo}
-              headerHeight={headerHeight}
-              onToggleLiked={onToggleLiked}
-              onScroll={onScroll}
-            />
-          )}
-        </PagerWithHeader>
+          ) : (
+            <CenteredView sideBorders style={[{paddingTop: headerHeight}]}>
+              <NonPublicFeedMessage />
+            </CenteredView>
+          )
+        }
+        {({onScroll, headerHeight, scrollElRef}) => (
+          <AboutSection
+            feedOwnerDid={feedInfo.creatorDid}
+            feedRkey={feedInfo.route.params.rkey}
+            feedInfo={feedInfo}
+            headerHeight={headerHeight}
+            onScroll={onScroll}
+            scrollElRef={
+              scrollElRef as React.MutableRefObject<ScrollView | null>
+            }
+            isOwner={feedInfo.creatorDid === currentAccount?.did}
+          />
+        )}
+      </PagerWithHeader>
+      {hasSession && (
         <FAB
           testID="composeFAB"
-          onPress={() => store.shell.openComposer({})}
+          onPress={() => openComposer({})}
           icon={
             <ComposeIcon2
               strokeWidth={1.5}
@@ -372,32 +447,67 @@ export const ProfileFeedScreenInner = observer(
             />
           }
           accessibilityRole="button"
-          accessibilityLabel="New post"
+          accessibilityLabel={_(msg`New post`)}
           accessibilityHint=""
         />
+      )}
+    </View>
+  )
+}
+
+function NonPublicFeedMessage() {
+  const pal = usePalette('default')
+
+  return (
+    <View
+      style={[
+        pal.border,
+        {
+          padding: 18,
+          borderTopWidth: 1,
+          minHeight: Dimensions.get('window').height * 1.5,
+        },
+      ]}>
+      <View
+        style={[
+          pal.viewLight,
+          {
+            padding: 12,
+            borderRadius: 8,
+          },
+        ]}>
+        <Text style={[pal.text]}>
+          <Trans>
+            Looks like this feed is only available to users with a Bluesky
+            account. Please sign up or sign in to view this feed!
+          </Trans>
+        </Text>
       </View>
-    )
-  },
-)
+    </View>
+  )
+}
 
 interface FeedSectionProps {
-  feed: PostsFeedModel
-  onScroll: (e: NativeScrollEvent) => void
+  feed: FeedDescriptor
+  onScroll: OnScrollHandler
   headerHeight: number
   isScrolledDown: boolean
+  scrollElRef: React.MutableRefObject<FlatList<any> | null>
+  isFocused: boolean
 }
 const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
   function FeedSectionImpl(
-    {feed, onScroll, headerHeight, isScrolledDown},
+    {feed, onScroll, headerHeight, isScrolledDown, scrollElRef, isFocused},
     ref,
   ) {
-    const hasNew = feed.hasNewLatest && !feed.isRefreshing
-    const scrollElRef = React.useRef<FlatList>(null)
+    const [hasNew, setHasNew] = React.useState(false)
+    const queryClient = useQueryClient()
 
     const onScrollToTop = useCallback(() => {
       scrollElRef.current?.scrollToOffset({offset: -headerHeight})
-      feed.refresh()
-    }, [feed, scrollElRef, headerHeight])
+      truncateAndInvalidate(queryClient, FEED_RQKEY(feed))
+      setHasNew(false)
+    }, [scrollElRef, headerHeight, queryClient, feed, setHasNew])
 
     React.useImperativeHandle(ref, () => ({
       scrollToTop: onScrollToTop,
@@ -407,13 +517,15 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
       return <EmptyState icon="feed" message="This feed is empty!" />
     }, [])
 
-    const scrollHandler = useAnimatedScrollHandler({onScroll})
     return (
       <View>
         <Feed
+          enabled={isFocused}
           feed={feed}
+          pollInterval={30e3}
           scrollElRef={scrollElRef}
-          onScroll={scrollHandler}
+          onHasNew={setHasNew}
+          onScroll={onScroll}
           scrollEventThrottle={5}
           renderEmptyState={renderPostsEmpty}
           headerOffset={headerHeight}
@@ -430,32 +542,64 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
   },
 )
 
-const AboutSection = observer(function AboutPageImpl({
+function AboutSection({
   feedOwnerDid,
   feedRkey,
   feedInfo,
   headerHeight,
-  onToggleLiked,
   onScroll,
+  scrollElRef,
+  isOwner,
 }: {
   feedOwnerDid: string
   feedRkey: string
-  feedInfo: FeedSourceModel | undefined
+  feedInfo: FeedSourceFeedInfo
   headerHeight: number
-  onToggleLiked: () => void
-  onScroll: (e: NativeScrollEvent) => void
+  onScroll: OnScrollHandler
+  scrollElRef: React.MutableRefObject<ScrollView | null>
+  isOwner: boolean
 }) {
   const pal = usePalette('default')
-  const scrollHandler = useAnimatedScrollHandler({onScroll})
+  const {_} = useLingui()
+  const scrollHandler = useAnimatedScrollHandler(onScroll)
+  const [likeUri, setLikeUri] = React.useState(feedInfo.likeUri)
+  const {hasSession} = useSession()
 
-  if (!feedInfo) {
-    return <View />
-  }
+  const {mutateAsync: likeFeed, isPending: isLikePending} = useLikeMutation()
+  const {mutateAsync: unlikeFeed, isPending: isUnlikePending} =
+    useUnlikeMutation()
+
+  const isLiked = !!likeUri
+  const likeCount =
+    isLiked && likeUri ? (feedInfo.likeCount || 0) + 1 : feedInfo.likeCount
+
+  const onToggleLiked = React.useCallback(async () => {
+    try {
+      Haptics.default()
+
+      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(
+        'There was an an issue contacting the server, please check your internet connection and try again.',
+      )
+      logger.error('Failed up toggle like', {error: err})
+    }
+  }, [likeUri, isLiked, feedInfo, likeFeed, unlikeFeed])
 
   return (
     <ScrollView
+      ref={scrollElRef}
       scrollEventThrottle={1}
-      contentContainerStyle={{paddingTop: headerHeight}}
+      contentContainerStyle={{
+        paddingTop: headerHeight,
+        minHeight: Dimensions.get('window').height * 1.5,
+      }}
       onScroll={scrollHandler}>
       <View
         style={[
@@ -467,46 +611,44 @@ const AboutSection = observer(function AboutPageImpl({
           },
           pal.border,
         ]}>
-        {feedInfo.descriptionRT ? (
+        {feedInfo.description ? (
           <RichText
             testID="listDescription"
             type="lg"
             style={pal.text}
-            richText={feedInfo.descriptionRT}
+            richText={feedInfo.description}
           />
         ) : (
           <Text type="lg" style={[{fontStyle: 'italic'}, pal.textLight]}>
-            No description
+            <Trans>No description</Trans>
           </Text>
         )}
         <View style={{flexDirection: 'row', alignItems: 'center', gap: 10}}>
           <Button
             type="default"
             testID="toggleLikeBtn"
-            accessibilityLabel="Like this feed"
+            accessibilityLabel={_(msg`Like this feed`)}
             accessibilityHint=""
+            disabled={!hasSession || isLikePending || isUnlikePending}
             onPress={onToggleLiked}
             style={{paddingHorizontal: 10}}>
-            {feedInfo?.isLiked ? (
+            {isLiked ? (
               <HeartIconSolid size={19} style={styles.liked} />
             ) : (
               <HeartIcon strokeWidth={3} size={19} style={pal.textLight} />
             )}
           </Button>
-          {typeof feedInfo.likeCount === 'number' && (
+          {typeof likeCount === 'number' && (
             <TextLink
               href={makeCustomFeedLink(feedOwnerDid, feedRkey, 'liked-by')}
-              text={`Liked by ${feedInfo.likeCount} ${pluralize(
-                feedInfo.likeCount,
-                'user',
-              )}`}
+              text={`Liked by ${likeCount} ${pluralize(likeCount, 'user')}`}
               style={[pal.textLight, s.semiBold]}
             />
           )}
         </View>
         <Text type="md" style={[pal.textLight]} numberOfLines={1}>
           Created by{' '}
-          {feedInfo.isOwner ? (
+          {isOwner ? (
             'you'
           ) : (
             <TextLink
@@ -522,7 +664,7 @@ const AboutSection = observer(function AboutPageImpl({
       </View>
     </ScrollView>
   )
-})
+}
 
 const styles = StyleSheet.create({
   btn: {