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>
zio/stable
Eric Bailey 2023-12-07 15:44:22 -06:00 committed by GitHub
parent 940fc0ea5c
commit 174a1622c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 112 additions and 83 deletions

View File

@ -16,6 +16,7 @@
* 3. Don't call this query's `refetch()` if you're trying to sync latest; call `checkUnread()` instead. * 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 {AppBskyFeedDefs} from '@atproto/api'
import { import {
useInfiniteQuery, useInfiniteQuery,
@ -49,7 +50,7 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
const unreads = useUnreadNotificationsApi() const unreads = useUnreadNotificationsApi()
const enabled = opts?.enabled !== false const enabled = opts?.enabled !== false
return useInfiniteQuery< const query = useInfiniteQuery<
FeedPage, FeedPage,
Error, Error,
InfiniteData<FeedPage>, InfiniteData<FeedPage>,
@ -85,6 +86,21 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
getNextPageParam: lastPage => lastPage.cursor, getNextPageParam: lastPage => lastPage.cursor,
enabled, 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
} }
/** /**

View File

@ -1,5 +1,10 @@
import {useCallback} from 'react' import {useCallback, useEffect} from 'react'
import {AppBskyFeedDefs, AppBskyFeedPost, moderatePost} from '@atproto/api' import {
AppBskyFeedDefs,
AppBskyFeedPost,
moderatePost,
PostModeration,
} from '@atproto/api'
import { import {
useInfiniteQuery, useInfiniteQuery,
InfiniteData, InfiniteData,
@ -24,6 +29,7 @@ import {DEFAULT_LOGGED_OUT_PREFERENCES} from '#/state/queries/preferences/const'
import {getModerationOpts} from '#/state/queries/preferences/moderation' import {getModerationOpts} from '#/state/queries/preferences/moderation'
import {KnownError} from '#/view/com/posts/FeedErrorMessage' import {KnownError} from '#/view/com/posts/FeedErrorMessage'
import {embedViewRecordToPostView, getEmbeddedPost} from './util' import {embedViewRecordToPostView, getEmbeddedPost} from './util'
import {useModerationOpts} from './preferences'
type ActorDid = string type ActorDid = string
type AuthorFilter = type AuthorFilter =
@ -57,6 +63,7 @@ export interface FeedPostSliceItem {
post: AppBskyFeedDefs.PostView post: AppBskyFeedDefs.PostView
record: AppBskyFeedPost.Record record: AppBskyFeedPost.Record
reason?: AppBskyFeedDefs.ReasonRepost | ReasonFeedSource reason?: AppBskyFeedDefs.ReasonRepost | ReasonFeedSource
moderation: PostModeration
} }
export interface FeedPostSlice { export interface FeedPostSlice {
@ -79,16 +86,19 @@ export interface FeedPage {
slices: FeedPostSlice[] slices: FeedPostSlice[]
} }
const PAGE_SIZE = 30
export function usePostFeedQuery( export function usePostFeedQuery(
feedDesc: FeedDescriptor, feedDesc: FeedDescriptor,
params?: FeedParams, params?: FeedParams,
opts?: {enabled?: boolean}, opts?: {enabled?: boolean; ignoreFilterFor?: string},
) { ) {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const feedTuners = useFeedTuners(feedDesc) 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, FeedPageUnselected,
Error, Error,
InfiniteData<FeedPage>, InfiniteData<FeedPage>,
@ -108,7 +118,7 @@ export function usePostFeedQuery(
cursor: undefined, 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 precacheResolvedUris(queryClient, res.feed) // precache the handle->did resolution
/* /*
@ -146,40 +156,79 @@ export function usePostFeedQuery(
api: page.api, api: page.api,
tuner, tuner,
cursor: page.cursor, cursor: page.cursor,
slices: tuner.tune(page.feed).map(slice => ({ slices: tuner
_reactKey: slice._reactKey, .tune(page.feed)
rootUri: slice.rootItem.post.uri, .map(slice => {
isThread: const moderations = slice.items.map(item =>
slice.items.length > 1 && moderatePost(item.post, moderationOpts!),
slice.items.every( )
item =>
item.post.author.did === slice.items[0].post.author.did, // apply moderation filter
), for (let i = 0; i < slice.items.length; i++) {
items: slice.items
.map((item, i) => {
if ( if (
AppBskyFeedPost.isRecord(item.post.record) && moderations[i]?.content.filter &&
AppBskyFeedPost.validateRecord(item.post.record).success slice.items[i].post.author.did !== opts?.ignoreFilterFor
) { ) {
return { return undefined
_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 }
})
.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) { export async function pollLatest(page: FeedPage | undefined) {

View File

@ -27,7 +27,6 @@ import {
usePostFeedQuery, usePostFeedQuery,
pollLatest, pollLatest,
} from '#/state/queries/post-feed' } from '#/state/queries/post-feed'
import {useModerationOpts} from '#/state/queries/preferences'
import {isWeb} from '#/platform/detection' import {isWeb} from '#/platform/detection'
import {listenPostCreated} from '#/state/events' import {listenPostCreated} from '#/state/events'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
@ -82,8 +81,10 @@ let Feed = ({
const [isPTRing, setIsPTRing] = React.useState(false) const [isPTRing, setIsPTRing] = React.useState(false)
const checkForNewRef = React.useRef<(() => void) | null>(null) const checkForNewRef = React.useRef<(() => void) | null>(null)
const moderationOpts = useModerationOpts() const opts = React.useMemo(
const opts = React.useMemo(() => ({enabled}), [enabled]) () => ({enabled, ignoreFilterFor}),
[enabled, ignoreFilterFor],
)
const { const {
data, data,
isFetching, isFetching,
@ -144,7 +145,7 @@ let Feed = ({
const feedItems = React.useMemo(() => { const feedItems = React.useMemo(() => {
let arr: any[] = [] let arr: any[] = []
if (isFetched && moderationOpts) { if (isFetched) {
if (isError && isEmpty) { if (isError && isEmpty) {
arr = arr.concat([ERROR_ITEM]) arr = arr.concat([ERROR_ITEM])
} }
@ -162,7 +163,7 @@ let Feed = ({
arr.push(LOADING_ITEM) arr.push(LOADING_ITEM)
} }
return arr return arr
}, [isFetched, isError, isEmpty, data, moderationOpts]) }, [isFetched, isError, isEmpty, data])
// events // events
// = // =
@ -224,24 +225,9 @@ let Feed = ({
} else if (item === LOADING_ITEM) { } else if (item === LOADING_ITEM) {
return <PostFeedLoadingPlaceholder /> return <PostFeedLoadingPlaceholder />
} }
return ( return <FeedSlice slice={item} />
<FeedSlice
slice={item}
// we check for this before creating the feedItems array
moderationOpts={moderationOpts!}
ignoreFilterFor={ignoreFilterFor}
/>
)
}, },
[ [feed, error, onPressTryAgain, onPressRetryLoadMore, renderEmptyState],
feed,
error,
onPressTryAgain,
onPressRetryLoadMore,
renderEmptyState,
moderationOpts,
ignoreFilterFor,
],
) )
const shouldRenderEndOfFeed = const shouldRenderEndOfFeed =

View File

@ -1,7 +1,7 @@
import React, {memo} from 'react' import React, {memo} from 'react'
import {StyleSheet, View} from 'react-native' import {StyleSheet, View} from 'react-native'
import {FeedPostSlice} from '#/state/queries/post-feed' 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 {Link} from '../util/Link'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import Svg, {Circle, Line} from 'react-native-svg' import Svg, {Circle, Line} from 'react-native-svg'
@ -9,29 +9,7 @@ import {FeedItem} from './FeedItem'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {makeProfileLink} from 'lib/routes/links' import {makeProfileLink} from 'lib/routes/links'
let FeedSlice = ({ let FeedSlice = ({slice}: {slice: FeedPostSlice}): React.ReactNode => {
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
}
}
if (slice.isThread && slice.items.length > 3) { if (slice.isThread && slice.items.length > 3) {
const last = slice.items.length - 1 const last = slice.items.length - 1
return ( return (
@ -41,7 +19,7 @@ let FeedSlice = ({
post={slice.items[0].post} post={slice.items[0].post}
record={slice.items[0].record} record={slice.items[0].record}
reason={slice.items[0].reason} reason={slice.items[0].reason}
moderation={moderations[0]} moderation={slice.items[0].moderation}
isThreadParent={isThreadParentAt(slice.items, 0)} isThreadParent={isThreadParentAt(slice.items, 0)}
isThreadChild={isThreadChildAt(slice.items, 0)} isThreadChild={isThreadChildAt(slice.items, 0)}
/> />
@ -50,7 +28,7 @@ let FeedSlice = ({
post={slice.items[1].post} post={slice.items[1].post}
record={slice.items[1].record} record={slice.items[1].record}
reason={slice.items[1].reason} reason={slice.items[1].reason}
moderation={moderations[1]} moderation={slice.items[1].moderation}
isThreadParent={isThreadParentAt(slice.items, 1)} isThreadParent={isThreadParentAt(slice.items, 1)}
isThreadChild={isThreadChildAt(slice.items, 1)} isThreadChild={isThreadChildAt(slice.items, 1)}
/> />
@ -60,7 +38,7 @@ let FeedSlice = ({
post={slice.items[last].post} post={slice.items[last].post}
record={slice.items[last].record} record={slice.items[last].record}
reason={slice.items[last].reason} reason={slice.items[last].reason}
moderation={moderations[last]} moderation={slice.items[last].moderation}
isThreadParent={isThreadParentAt(slice.items, last)} isThreadParent={isThreadParentAt(slice.items, last)}
isThreadChild={isThreadChildAt(slice.items, last)} isThreadChild={isThreadChildAt(slice.items, last)}
isThreadLastChild isThreadLastChild
@ -77,7 +55,7 @@ let FeedSlice = ({
post={slice.items[i].post} post={slice.items[i].post}
record={slice.items[i].record} record={slice.items[i].record}
reason={slice.items[i].reason} reason={slice.items[i].reason}
moderation={moderations[i]} moderation={slice.items[i].moderation}
isThreadParent={isThreadParentAt(slice.items, i)} isThreadParent={isThreadParentAt(slice.items, i)}
isThreadChild={isThreadChildAt(slice.items, i)} isThreadChild={isThreadChildAt(slice.items, i)}
isThreadLastChild={ isThreadLastChild={