about summary refs log tree commit diff
diff options
context:
space:
mode:
authorEric Bailey <git@esb.lol>2023-12-07 15:44:22 -0600
committerGitHub <noreply@github.com>2023-12-07 13:44:22 -0800
commit174a1622c9dc34d580d3deb1f52f85aec4ac9f42 (patch)
treed9b6ffe6c8ba20500f55ec7256282e09064fa45f
parent940fc0ea5c3aefb49f39d8de9398a306882ed567 (diff)
downloadvoidsky-174a1622c9dc34d580d3deb1f52f85aec4ac9f42.tar.zst
Hoist moderation, attempt to fill feed up to 30 (#2134)
* Move moderatePost up to feed query

* Attemt to fill page up to 30

* Add the 'ensure full page' behavior to notifs

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
-rw-r--r--src/state/queries/notifications/feed.ts18
-rw-r--r--src/state/queries/post-feed.ts113
-rw-r--r--src/view/com/posts/Feed.tsx30
-rw-r--r--src/view/com/posts/FeedSlice.tsx34
4 files changed, 112 insertions, 83 deletions
diff --git a/src/state/queries/notifications/feed.ts b/src/state/queries/notifications/feed.ts
index d2c150c31..3f9700c79 100644
--- a/src/state/queries/notifications/feed.ts
+++ b/src/state/queries/notifications/feed.ts
@@ -16,6 +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 {AppBskyFeedDefs} from '@atproto/api'
 import {
   useInfiniteQuery,
@@ -49,7 +50,7 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
   const unreads = useUnreadNotificationsApi()
   const enabled = opts?.enabled !== false
 
-  return useInfiniteQuery<
+  const query = useInfiniteQuery<
     FeedPage,
     Error,
     InfiniteData<FeedPage>,
@@ -85,6 +86,21 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
     getNextPageParam: lastPage => lastPage.cursor,
     enabled,
   })
+
+  useEffect(() => {
+    const {isFetching, hasNextPage, data} = query
+
+    let count = 0
+    for (const page of data?.pages || []) {
+      count += page.items.length
+    }
+
+    if (!isFetching && hasNextPage && count < PAGE_SIZE) {
+      query.fetchNextPage()
+    }
+  }, [query])
+
+  return query
 }
 
 /**
diff --git a/src/state/queries/post-feed.ts b/src/state/queries/post-feed.ts
index de18865ea..dede8a956 100644
--- a/src/state/queries/post-feed.ts
+++ b/src/state/queries/post-feed.ts
@@ -1,5 +1,10 @@
-import {useCallback} from 'react'
-import {AppBskyFeedDefs, AppBskyFeedPost, moderatePost} from '@atproto/api'
+import {useCallback, useEffect} from 'react'
+import {
+  AppBskyFeedDefs,
+  AppBskyFeedPost,
+  moderatePost,
+  PostModeration,
+} from '@atproto/api'
 import {
   useInfiniteQuery,
   InfiniteData,
@@ -24,6 +29,7 @@ import {DEFAULT_LOGGED_OUT_PREFERENCES} from '#/state/queries/preferences/const'
 import {getModerationOpts} from '#/state/queries/preferences/moderation'
 import {KnownError} from '#/view/com/posts/FeedErrorMessage'
 import {embedViewRecordToPostView, getEmbeddedPost} from './util'
+import {useModerationOpts} from './preferences'
 
 type ActorDid = string
 type AuthorFilter =
@@ -57,6 +63,7 @@ export interface FeedPostSliceItem {
   post: AppBskyFeedDefs.PostView
   record: AppBskyFeedPost.Record
   reason?: AppBskyFeedDefs.ReasonRepost | ReasonFeedSource
+  moderation: PostModeration
 }
 
 export interface FeedPostSlice {
@@ -79,16 +86,19 @@ export interface FeedPage {
   slices: FeedPostSlice[]
 }
 
+const PAGE_SIZE = 30
+
 export function usePostFeedQuery(
   feedDesc: FeedDescriptor,
   params?: FeedParams,
-  opts?: {enabled?: boolean},
+  opts?: {enabled?: boolean; ignoreFilterFor?: string},
 ) {
   const queryClient = useQueryClient()
   const feedTuners = useFeedTuners(feedDesc)
-  const enabled = opts?.enabled !== false
+  const moderationOpts = useModerationOpts()
+  const enabled = opts?.enabled !== false && Boolean(moderationOpts)
 
-  return useInfiniteQuery<
+  const query = useInfiniteQuery<
     FeedPageUnselected,
     Error,
     InfiniteData<FeedPage>,
@@ -108,7 +118,7 @@ export function usePostFeedQuery(
             cursor: undefined,
           }
 
-      const res = await api.fetch({cursor, limit: 30})
+      const res = await api.fetch({cursor, limit: PAGE_SIZE})
       precacheResolvedUris(queryClient, res.feed) // precache the handle->did resolution
 
       /*
@@ -146,40 +156,79 @@ export function usePostFeedQuery(
             api: page.api,
             tuner,
             cursor: page.cursor,
-            slices: tuner.tune(page.feed).map(slice => ({
-              _reactKey: slice._reactKey,
-              rootUri: slice.rootItem.post.uri,
-              isThread:
-                slice.items.length > 1 &&
-                slice.items.every(
-                  item =>
-                    item.post.author.did === slice.items[0].post.author.did,
-                ),
-              items: slice.items
-                .map((item, i) => {
+            slices: tuner
+              .tune(page.feed)
+              .map(slice => {
+                const moderations = slice.items.map(item =>
+                  moderatePost(item.post, moderationOpts!),
+                )
+
+                // apply moderation filter
+                for (let i = 0; i < slice.items.length; i++) {
                   if (
-                    AppBskyFeedPost.isRecord(item.post.record) &&
-                    AppBskyFeedPost.validateRecord(item.post.record).success
+                    moderations[i]?.content.filter &&
+                    slice.items[i].post.author.did !== opts?.ignoreFilterFor
                   ) {
-                    return {
-                      _reactKey: `${slice._reactKey}-${i}`,
-                      uri: item.post.uri,
-                      post: item.post,
-                      record: item.post.record,
-                      reason:
-                        i === 0 && slice.source ? slice.source : item.reason,
-                    }
+                    return undefined
                   }
-                  return undefined
-                })
-                .filter(Boolean) as FeedPostSliceItem[],
-            })),
+                }
+
+                return {
+                  _reactKey: slice._reactKey,
+                  rootUri: slice.rootItem.post.uri,
+                  isThread:
+                    slice.items.length > 1 &&
+                    slice.items.every(
+                      item =>
+                        item.post.author.did === slice.items[0].post.author.did,
+                    ),
+                  items: slice.items
+                    .map((item, i) => {
+                      if (
+                        AppBskyFeedPost.isRecord(item.post.record) &&
+                        AppBskyFeedPost.validateRecord(item.post.record).success
+                      ) {
+                        return {
+                          _reactKey: `${slice._reactKey}-${i}`,
+                          uri: item.post.uri,
+                          post: item.post,
+                          record: item.post.record,
+                          reason:
+                            i === 0 && slice.source
+                              ? slice.source
+                              : item.reason,
+                          moderation: moderations[i],
+                        }
+                      }
+                      return undefined
+                    })
+                    .filter(Boolean) as FeedPostSliceItem[],
+                }
+              })
+              .filter(Boolean) as FeedPostSlice[],
           })),
         }
       },
-      [feedTuners, params?.disableTuner],
+      [feedTuners, params?.disableTuner, moderationOpts, opts?.ignoreFilterFor],
     ),
   })
+
+  useEffect(() => {
+    const {isFetching, hasNextPage, data} = query
+
+    let count = 0
+    for (const page of data?.pages || []) {
+      for (const slice of page.slices) {
+        count += slice.items.length
+      }
+    }
+
+    if (!isFetching && hasNextPage && count < PAGE_SIZE) {
+      query.fetchNextPage()
+    }
+  }, [query])
+
+  return query
 }
 
 export async function pollLatest(page: FeedPage | undefined) {
diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx
index 371f73b87..03fa95eea 100644
--- a/src/view/com/posts/Feed.tsx
+++ b/src/view/com/posts/Feed.tsx
@@ -27,7 +27,6 @@ import {
   usePostFeedQuery,
   pollLatest,
 } from '#/state/queries/post-feed'
-import {useModerationOpts} from '#/state/queries/preferences'
 import {isWeb} from '#/platform/detection'
 import {listenPostCreated} from '#/state/events'
 import {useSession} from '#/state/session'
@@ -82,8 +81,10 @@ let Feed = ({
   const [isPTRing, setIsPTRing] = React.useState(false)
   const checkForNewRef = React.useRef<(() => void) | null>(null)
 
-  const moderationOpts = useModerationOpts()
-  const opts = React.useMemo(() => ({enabled}), [enabled])
+  const opts = React.useMemo(
+    () => ({enabled, ignoreFilterFor}),
+    [enabled, ignoreFilterFor],
+  )
   const {
     data,
     isFetching,
@@ -144,7 +145,7 @@ let Feed = ({
 
   const feedItems = React.useMemo(() => {
     let arr: any[] = []
-    if (isFetched && moderationOpts) {
+    if (isFetched) {
       if (isError && isEmpty) {
         arr = arr.concat([ERROR_ITEM])
       }
@@ -162,7 +163,7 @@ let Feed = ({
       arr.push(LOADING_ITEM)
     }
     return arr
-  }, [isFetched, isError, isEmpty, data, moderationOpts])
+  }, [isFetched, isError, isEmpty, data])
 
   // events
   // =
@@ -224,24 +225,9 @@ let Feed = ({
       } else if (item === LOADING_ITEM) {
         return <PostFeedLoadingPlaceholder />
       }
-      return (
-        <FeedSlice
-          slice={item}
-          // we check for this before creating the feedItems array
-          moderationOpts={moderationOpts!}
-          ignoreFilterFor={ignoreFilterFor}
-        />
-      )
+      return <FeedSlice slice={item} />
     },
-    [
-      feed,
-      error,
-      onPressTryAgain,
-      onPressRetryLoadMore,
-      renderEmptyState,
-      moderationOpts,
-      ignoreFilterFor,
-    ],
+    [feed, error, onPressTryAgain, onPressRetryLoadMore, renderEmptyState],
   )
 
   const shouldRenderEndOfFeed =
diff --git a/src/view/com/posts/FeedSlice.tsx b/src/view/com/posts/FeedSlice.tsx
index a3bacdc1e..c1a8c0e18 100644
--- a/src/view/com/posts/FeedSlice.tsx
+++ b/src/view/com/posts/FeedSlice.tsx
@@ -1,7 +1,7 @@
 import React, {memo} from 'react'
 import {StyleSheet, View} from 'react-native'
 import {FeedPostSlice} from '#/state/queries/post-feed'
-import {AtUri, moderatePost, ModerationOpts} from '@atproto/api'
+import {AtUri} from '@atproto/api'
 import {Link} from '../util/Link'
 import {Text} from '../util/text/Text'
 import Svg, {Circle, Line} from 'react-native-svg'
@@ -9,29 +9,7 @@ import {FeedItem} from './FeedItem'
 import {usePalette} from 'lib/hooks/usePalette'
 import {makeProfileLink} from 'lib/routes/links'
 
-let FeedSlice = ({
-  slice,
-  ignoreFilterFor,
-  moderationOpts,
-}: {
-  slice: FeedPostSlice
-  ignoreFilterFor?: string
-  moderationOpts: ModerationOpts
-}): React.ReactNode => {
-  const moderations = React.useMemo(() => {
-    return slice.items.map(item => moderatePost(item.post, moderationOpts))
-  }, [slice, moderationOpts])
-
-  // apply moderation filter
-  for (let i = 0; i < slice.items.length; i++) {
-    if (
-      moderations[i]?.content.filter &&
-      slice.items[i].post.author.did !== ignoreFilterFor
-    ) {
-      return null
-    }
-  }
-
+let FeedSlice = ({slice}: {slice: FeedPostSlice}): React.ReactNode => {
   if (slice.isThread && slice.items.length > 3) {
     const last = slice.items.length - 1
     return (
@@ -41,7 +19,7 @@ let FeedSlice = ({
           post={slice.items[0].post}
           record={slice.items[0].record}
           reason={slice.items[0].reason}
-          moderation={moderations[0]}
+          moderation={slice.items[0].moderation}
           isThreadParent={isThreadParentAt(slice.items, 0)}
           isThreadChild={isThreadChildAt(slice.items, 0)}
         />
@@ -50,7 +28,7 @@ let FeedSlice = ({
           post={slice.items[1].post}
           record={slice.items[1].record}
           reason={slice.items[1].reason}
-          moderation={moderations[1]}
+          moderation={slice.items[1].moderation}
           isThreadParent={isThreadParentAt(slice.items, 1)}
           isThreadChild={isThreadChildAt(slice.items, 1)}
         />
@@ -60,7 +38,7 @@ let FeedSlice = ({
           post={slice.items[last].post}
           record={slice.items[last].record}
           reason={slice.items[last].reason}
-          moderation={moderations[last]}
+          moderation={slice.items[last].moderation}
           isThreadParent={isThreadParentAt(slice.items, last)}
           isThreadChild={isThreadChildAt(slice.items, last)}
           isThreadLastChild
@@ -77,7 +55,7 @@ let FeedSlice = ({
           post={slice.items[i].post}
           record={slice.items[i].record}
           reason={slice.items[i].reason}
-          moderation={moderations[i]}
+          moderation={slice.items[i].moderation}
           isThreadParent={isThreadParentAt(slice.items, i)}
           isThreadChild={isThreadChildAt(slice.items, i)}
           isThreadLastChild={