about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/state/queries/feed.ts45
-rw-r--r--src/state/queries/notifications/feed.ts29
-rw-r--r--src/state/queries/post-feed.ts35
-rw-r--r--src/state/queries/profile.ts4
-rw-r--r--src/view/com/feeds/FeedPage.tsx2
-rw-r--r--src/view/com/posts/Feed.tsx26
-rw-r--r--src/view/screens/ProfileFeed.tsx84
-rw-r--r--src/view/screens/ProfileList.tsx7
8 files changed, 82 insertions, 150 deletions
diff --git a/src/state/queries/feed.ts b/src/state/queries/feed.ts
index e431643e7..c9d81bc17 100644
--- a/src/state/queries/feed.ts
+++ b/src/state/queries/feed.ts
@@ -161,51 +161,6 @@ export function useFeedSourceInfoQuery({uri}: {uri: string}) {
   })
 }
 
-export const isFeedPublicQueryKey = ({uri}: {uri: string}) => [
-  'isFeedPublic',
-  uri,
-]
-
-export function useIsFeedPublicQuery({uri}: {uri: string}) {
-  return useQuery({
-    queryKey: isFeedPublicQueryKey({uri}),
-    queryFn: async ({queryKey}) => {
-      const [, uri] = queryKey
-      try {
-        const res = await getAgent().app.bsky.feed.getFeed({
-          feed: uri,
-          limit: 1,
-        })
-        return {
-          isPublic: Boolean(res.data.feed),
-          error: undefined,
-        }
-      } catch (e: any) {
-        /**
-         * This should be an `XRPCError`, but I can't safely import from
-         * `@atproto/xrpc` due to a depdency on node's `crypto` module.
-         *
-         * @see https://github.com/bluesky-social/atproto/blob/c17971a2d8e424cc7f10c071d97c07c08aa319cf/packages/xrpc/src/client.ts#L126
-         */
-        if (e?.status === 401) {
-          return {
-            isPublic: false,
-            error: e,
-          }
-        }
-
-        /*
-         * Non-401 response means something else went wrong on the server
-         */
-        return {
-          isPublic: true,
-          error: e,
-        }
-      }
-    },
-  })
-}
-
 export const useGetPopularFeedsQueryKey = ['getPopularFeeds']
 
 export function useGetPopularFeedsQuery() {
diff --git a/src/state/queries/notifications/feed.ts b/src/state/queries/notifications/feed.ts
index dc206df79..d652f493d 100644
--- a/src/state/queries/notifications/feed.ts
+++ b/src/state/queries/notifications/feed.ts
@@ -16,7 +16,7 @@
  * 3. Don't call this query's `refetch()` if you're trying to sync latest; call `checkUnread()` instead.
  */
 
-import {useEffect} from 'react'
+import {useEffect, useRef} from 'react'
 import {AppBskyFeedDefs} from '@atproto/api'
 import {
   useInfiniteQuery,
@@ -49,6 +49,7 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
   const threadMutes = useMutedThreads()
   const unreads = useUnreadNotificationsApi()
   const enabled = opts?.enabled !== false
+  const lastPageCountRef = useRef(0)
 
   const query = useInfiniteQuery<
     FeedPage,
@@ -104,24 +105,26 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
 
   useEffect(() => {
     const {isFetching, hasNextPage, data} = query
+    if (isFetching || !hasNextPage) {
+      return
+    }
+
+    // avoid double-fires of fetchNextPage()
+    if (
+      lastPageCountRef.current !== 0 &&
+      lastPageCountRef.current === data?.pages?.length
+    ) {
+      return
+    }
 
+    // fetch next page if we haven't gotten a full page of content
     let count = 0
-    let numEmpties = 0
     for (const page of data?.pages || []) {
-      if (!page.items.length) {
-        numEmpties++
-      }
       count += page.items.length
     }
-
-    if (
-      !isFetching &&
-      hasNextPage &&
-      count < PAGE_SIZE &&
-      numEmpties < 3 &&
-      (data?.pages.length || 0) < 6
-    ) {
+    if (count < PAGE_SIZE && (data?.pages.length || 0) < 6) {
       query.fetchNextPage()
+      lastPageCountRef.current = data?.pages?.length || 0
     }
   }, [query])
 
diff --git a/src/state/queries/post-feed.ts b/src/state/queries/post-feed.ts
index 423de4ae8..b91af372f 100644
--- a/src/state/queries/post-feed.ts
+++ b/src/state/queries/post-feed.ts
@@ -1,4 +1,4 @@
-import React, {useCallback, useEffect} from 'react'
+import React, {useCallback, useEffect, useRef} from 'react'
 import {
   AppBskyFeedDefs,
   AppBskyFeedPost,
@@ -78,6 +78,7 @@ export interface FeedPageUnselected {
   api: FeedAPI
   cursor: string | undefined
   feed: AppBskyFeedDefs.FeedViewPost[]
+  fetchedAt: number
 }
 
 export interface FeedPage {
@@ -85,6 +86,7 @@ export interface FeedPage {
   tuner: FeedTuner | NoopFeedTuner
   cursor: string | undefined
   slices: FeedPostSlice[]
+  fetchedAt: number
 }
 
 const PAGE_SIZE = 30
@@ -98,11 +100,12 @@ export function usePostFeedQuery(
   const feedTuners = useFeedTuners(feedDesc)
   const moderationOpts = useModerationOpts()
   const enabled = opts?.enabled !== false && Boolean(moderationOpts)
-  const lastRun = React.useRef<{
+  const lastRun = useRef<{
     data: InfiniteData<FeedPageUnselected>
     args: typeof selectArgs
     result: InfiniteData<FeedPage>
   } | null>(null)
+  const lastPageCountRef = useRef(0)
 
   // Make sure this doesn't invalidate unless really needed.
   const selectArgs = React.useMemo(
@@ -152,6 +155,7 @@ export function usePostFeedQuery(
         api,
         cursor: res.cursor,
         feed: res.feed,
+        fetchedAt: Date.now(),
       }
     },
     initialPageParam: undefined,
@@ -214,6 +218,7 @@ export function usePostFeedQuery(
               api: page.api,
               tuner,
               cursor: page.cursor,
+              fetchedAt: page.fetchedAt,
               slices: tuner
                 .tune(page.feed)
                 .map(slice => {
@@ -279,26 +284,28 @@ export function usePostFeedQuery(
 
   useEffect(() => {
     const {isFetching, hasNextPage, data} = query
+    if (isFetching || !hasNextPage) {
+      return
+    }
+
+    // avoid double-fires of fetchNextPage()
+    if (
+      lastPageCountRef.current !== 0 &&
+      lastPageCountRef.current === data?.pages?.length
+    ) {
+      return
+    }
 
+    // fetch next page if we haven't gotten a full page of content
     let count = 0
-    let numEmpties = 0
     for (const page of data?.pages || []) {
-      if (page.slices.length === 0) {
-        numEmpties++
-      }
       for (const slice of page.slices) {
         count += slice.items.length
       }
     }
-
-    if (
-      !isFetching &&
-      hasNextPage &&
-      count < PAGE_SIZE &&
-      numEmpties < 3 &&
-      (data?.pages.length || 0) < 6
-    ) {
+    if (count < PAGE_SIZE && (data?.pages.length || 0) < 6) {
       query.fetchNextPage()
+      lastPageCountRef.current = data?.pages?.length || 0
     }
   }, [query])
 
diff --git a/src/state/queries/profile.ts b/src/state/queries/profile.ts
index 5fd0b4e34..40ba0653c 100644
--- a/src/state/queries/profile.ts
+++ b/src/state/queries/profile.ts
@@ -35,9 +35,7 @@ export function useProfileQuery({did}: {did: string | undefined}) {
     // if you remove it, the UI infinite-loops
     // -prf
     staleTime: isCurrentAccount ? STALE.SECONDS.THIRTY : STALE.MINUTES.FIVE,
-    refetchInterval: isCurrentAccount
-      ? STALE.SECONDS.THIRTY
-      : STALE.MINUTES.FIVE,
+    refetchInterval: STALE.MINUTES.FIVE,
     queryKey: RQKEY(did || ''),
     queryFn: async () => {
       const res = await getAgent().getProfile({actor: did || ''})
diff --git a/src/view/com/feeds/FeedPage.tsx b/src/view/com/feeds/FeedPage.tsx
index 9c92a0dd5..84d49e3b0 100644
--- a/src/view/com/feeds/FeedPage.tsx
+++ b/src/view/com/feeds/FeedPage.tsx
@@ -29,7 +29,7 @@ import {truncateAndInvalidate} from '#/state/queries/util'
 import {TabState, getTabState, getRootNavigation} from '#/lib/routes/helpers'
 import {isNative} from '#/platform/detection'
 
-const POLL_FREQ = 30e3 // 30sec
+const POLL_FREQ = 60e3 // 60sec
 
 export function FeedPage({
   testID,
diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx
index 9194bb163..8d5c11bda 100644
--- a/src/view/com/posts/Feed.tsx
+++ b/src/view/com/posts/Feed.tsx
@@ -29,12 +29,16 @@ import {
 import {isWeb} from '#/platform/detection'
 import {listenPostCreated} from '#/state/events'
 import {useSession} from '#/state/session'
+import {STALE} from '#/state/queries'
 
 const LOADING_ITEM = {_reactKey: '__loading__'}
 const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
 const ERROR_ITEM = {_reactKey: '__error__'}
 const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
 
+const REFRESH_AFTER = STALE.HOURS.ONE
+const CHECK_LATEST_AFTER = STALE.SECONDS.THIRTY
+
 let Feed = ({
   feed,
   feedParams,
@@ -77,6 +81,7 @@ let Feed = ({
   const {currentAccount} = useSession()
   const [isPTRing, setIsPTRing] = React.useState(false)
   const checkForNewRef = React.useRef<(() => void) | null>(null)
+  const lastFetchRef = React.useRef<number>(Date.now())
 
   const opts = React.useMemo(
     () => ({enabled, ignoreFilterFor}),
@@ -94,6 +99,9 @@ let Feed = ({
     fetchNextPage,
   } = usePostFeedQuery(feed, feedParams, opts)
   const isEmpty = !isFetching && !data?.pages[0]?.slices.length
+  if (data?.pages[0]) {
+    lastFetchRef.current = data?.pages[0].fetchedAt
+  }
 
   const checkForNew = React.useCallback(async () => {
     if (!data?.pages[0] || isFetching || !onHasNew || !enabled) {
@@ -133,11 +141,21 @@ let Feed = ({
     checkForNewRef.current = checkForNew
   }, [checkForNew])
   React.useEffect(() => {
-    if (enabled && checkForNewRef.current) {
-      // check for new on enable (aka on focus)
-      checkForNewRef.current()
+    if (enabled) {
+      const timeSinceFirstLoad = Date.now() - lastFetchRef.current
+      if (timeSinceFirstLoad > REFRESH_AFTER) {
+        // do a full refresh
+        scrollElRef?.current?.scrollToOffset({offset: 0, animated: false})
+        queryClient.resetQueries({queryKey: RQKEY(feed)})
+      } else if (
+        timeSinceFirstLoad > CHECK_LATEST_AFTER &&
+        checkForNewRef.current
+      ) {
+        // check for new on enable (aka on focus)
+        checkForNewRef.current()
+      }
     }
-  }, [enabled])
+  }, [enabled, feed, queryClient, scrollElRef])
   React.useEffect(() => {
     let cleanup1: () => void | undefined, cleanup2: () => void | undefined
     const subscription = AppState.addEventListener('change', nextAppState => {
diff --git a/src/view/screens/ProfileFeed.tsx b/src/view/screens/ProfileFeed.tsx
index ea92ebab1..061de08f2 100644
--- a/src/view/screens/ProfileFeed.tsx
+++ b/src/view/screens/ProfileFeed.tsx
@@ -1,7 +1,7 @@
 import React, {useMemo, useCallback} from 'react'
 import {Dimensions, StyleSheet, View, ActivityIndicator} from 'react-native'
 import {NativeStackScreenProps} from '@react-navigation/native-stack'
-import {useNavigation} from '@react-navigation/native'
+import {useIsFocused, useNavigation} from '@react-navigation/native'
 import {useQueryClient} from '@tanstack/react-query'
 import {usePalette} from 'lib/hooks/usePalette'
 import {HeartIcon, HeartIconSolid} from 'lib/icons'
@@ -42,11 +42,7 @@ import {logger} from '#/logger'
 import {Trans, msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useModalControls} from '#/state/modals'
-import {
-  useFeedSourceInfoQuery,
-  FeedSourceFeedInfo,
-  useIsFeedPublicQuery,
-} from '#/state/queries/feed'
+import {useFeedSourceInfoQuery, FeedSourceFeedInfo} from '#/state/queries/feed'
 import {useResolveUriQuery} from '#/state/queries/resolve-uri'
 import {
   UsePreferencesQueryResponse,
@@ -132,10 +128,8 @@ export function ProfileFeedScreen(props: Props) {
 function ProfileFeedScreenIntermediate({feedUri}: {feedUri: string}) {
   const {data: preferences} = usePreferencesQuery()
   const {data: info} = useFeedSourceInfoQuery({uri: feedUri})
-  const {isLoading: isPublicStatusLoading, data: isPublicResponse} =
-    useIsFeedPublicQuery({uri: feedUri})
 
-  if (!preferences || !info || isPublicStatusLoading) {
+  if (!preferences || !info) {
     return (
       <CenteredView>
         <View style={s.p20}>
@@ -149,7 +143,6 @@ function ProfileFeedScreenIntermediate({feedUri}: {feedUri: string}) {
     <ProfileFeedScreenInner
       preferences={preferences}
       feedInfo={info as FeedSourceFeedInfo}
-      isPublicResponse={isPublicResponse}
     />
   )
 }
@@ -157,11 +150,9 @@ function ProfileFeedScreenIntermediate({feedUri}: {feedUri: string}) {
 export function ProfileFeedScreenInner({
   preferences,
   feedInfo,
-  isPublicResponse,
 }: {
   preferences: UsePreferencesQueryResponse
   feedInfo: FeedSourceFeedInfo
-  isPublicResponse: ReturnType<typeof useIsFeedPublicQuery>['data']
 }) {
   const {_} = useLingui()
   const pal = usePalette('default')
@@ -170,6 +161,7 @@ export function ProfileFeedScreenInner({
   const {openComposer} = useComposerControls()
   const {track} = useAnalytics()
   const feedSectionRef = React.useRef<SectionRef>(null)
+  const isScreenFocused = useIsFocused()
 
   const {
     mutateAsync: saveFeed,
@@ -205,6 +197,9 @@ export function ProfileFeedScreenInner({
 
   useSetTitle(feedInfo?.displayName)
 
+  // event handlers
+  //
+
   const onToggleSaved = React.useCallback(async () => {
     try {
       Haptics.default()
@@ -398,21 +393,15 @@ export function ProfileFeedScreenInner({
         isHeaderReady={true}
         renderHeader={renderHeader}
         onCurrentPageSelected={onCurrentPageSelected}>
-        {({headerHeight, scrollElRef, isFocused}) =>
-          isPublicResponse?.isPublic ? (
-            <FeedSection
-              ref={feedSectionRef}
-              feed={`feedgen|${feedInfo.uri}`}
-              headerHeight={headerHeight}
-              scrollElRef={scrollElRef as ListRef}
-              isFocused={isFocused}
-            />
-          ) : (
-            <CenteredView sideBorders style={[{paddingTop: headerHeight}]}>
-              <NonPublicFeedMessage rawError={isPublicResponse?.error} />
-            </CenteredView>
-          )
-        }
+        {({headerHeight, scrollElRef, isFocused}) => (
+          <FeedSection
+            ref={feedSectionRef}
+            feed={`feedgen|${feedInfo.uri}`}
+            headerHeight={headerHeight}
+            scrollElRef={scrollElRef as ListRef}
+            isFocused={isScreenFocused && isFocused}
+          />
+        )}
         {({headerHeight, scrollElRef}) => (
           <AboutSection
             feedOwnerDid={feedInfo.creatorDid}
@@ -446,45 +435,6 @@ export function ProfileFeedScreenInner({
   )
 }
 
-function NonPublicFeedMessage({rawError}: {rawError?: Error}) {
-  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,
-            gap: 12,
-          },
-        ]}>
-        <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>
-
-        {rawError?.message && (
-          <Text style={pal.textLight}>
-            <Trans>Message from server</Trans>: {rawError.message}
-          </Text>
-        )}
-      </View>
-    </View>
-  )
-}
-
 interface FeedSectionProps {
   feed: FeedDescriptor
   headerHeight: number
@@ -519,7 +469,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
         <Feed
           enabled={isFocused}
           feed={feed}
-          pollInterval={30e3}
+          pollInterval={60e3}
           scrollElRef={scrollElRef}
           onHasNew={setHasNew}
           onScrolledDownChange={setIsScrolledDown}
diff --git a/src/view/screens/ProfileList.tsx b/src/view/screens/ProfileList.tsx
index 7f922e5b4..2db768cc5 100644
--- a/src/view/screens/ProfileList.tsx
+++ b/src/view/screens/ProfileList.tsx
@@ -1,6 +1,6 @@
 import React, {useCallback, useMemo} from 'react'
 import {ActivityIndicator, Pressable, StyleSheet, View} from 'react-native'
-import {useFocusEffect} from '@react-navigation/native'
+import {useFocusEffect, useIsFocused} from '@react-navigation/native'
 import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
 import {useNavigation} from '@react-navigation/native'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
@@ -115,6 +115,7 @@ function ProfileListScreenLoaded({
   const aboutSectionRef = React.useRef<SectionRef>(null)
   const {openModal} = useModalControls()
   const isCurateList = list.purpose === 'app.bsky.graph.defs#curatelist'
+  const isScreenFocused = useIsFocused()
 
   useSetTitle(list.name)
 
@@ -165,7 +166,7 @@ function ProfileListScreenLoaded({
               feed={`list|${uri}`}
               scrollElRef={scrollElRef as ListRef}
               headerHeight={headerHeight}
-              isFocused={isFocused}
+              isFocused={isScreenFocused && isFocused}
             />
           )}
           {({headerHeight, scrollElRef}) => (
@@ -623,7 +624,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
           testID="listFeed"
           enabled={isFocused}
           feed={feed}
-          pollInterval={30e3}
+          pollInterval={60e3}
           scrollElRef={scrollElRef}
           onHasNew={setHasNew}
           onScrolledDownChange={setIsScrolledDown}