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
parent
940fc0ea5c
commit
174a1622c9
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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 =
|
||||||
|
|
|
@ -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={
|
||||||
|
|
Loading…
Reference in New Issue