Merge remote-tracking branch 'origin/main' into hailey/oauth-exploration-web
This commit is contained in:
@@ -38,11 +38,8 @@ export function useFeedTuners(feedDesc: FeedDescriptor) {
|
||||
feedTuners.push(FeedTuner.removeReplies)
|
||||
} else {
|
||||
feedTuners.push(
|
||||
FeedTuner.thresholdRepliesOnly({
|
||||
FeedTuner.followedRepliesOnly({
|
||||
userDid: currentAccount?.did || '',
|
||||
minLikes: preferences?.feedViewPrefs.hideRepliesByLikeCount || 0,
|
||||
followedOnly:
|
||||
!!preferences?.feedViewPrefs.hideRepliesByUnfollowed,
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -66,10 +63,8 @@ export function useFeedTuners(feedDesc: FeedDescriptor) {
|
||||
feedTuners.push(FeedTuner.removeReplies)
|
||||
} else {
|
||||
feedTuners.push(
|
||||
FeedTuner.thresholdRepliesOnly({
|
||||
FeedTuner.followedRepliesOnly({
|
||||
userDid: currentAccount?.did || '',
|
||||
minLikes: preferences?.feedViewPrefs.hideRepliesByLikeCount || 0,
|
||||
followedOnly: !!preferences?.feedViewPrefs.hideRepliesByUnfollowed,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
+33
-20
@@ -5,6 +5,7 @@ import {
|
||||
AppBskyGraphDefs,
|
||||
AppBskyUnspeccedGetPopularFeedGenerators,
|
||||
AtUri,
|
||||
moderateFeedGenerator,
|
||||
RichText,
|
||||
} from '@atproto/api'
|
||||
import {
|
||||
@@ -26,6 +27,7 @@ import {RQKEY as listQueryKey} from '#/state/queries/list'
|
||||
import {usePreferencesQuery} from '#/state/queries/preferences'
|
||||
import {useAgent, useSession} from '#/state/session'
|
||||
import {router} from '#/routes'
|
||||
import {useModerationOpts} from '../preferences/moderation-opts'
|
||||
import {FeedDescriptor} from './post-feed'
|
||||
import {precacheResolvedUri} from './resolve-uri'
|
||||
|
||||
@@ -207,14 +209,16 @@ export function useGetPopularFeedsQuery(options?: GetPopularFeedsOptions) {
|
||||
const limit = options?.limit || 10
|
||||
const {data: preferences} = usePreferencesQuery()
|
||||
const queryClient = useQueryClient()
|
||||
const moderationOpts = useModerationOpts()
|
||||
|
||||
// Make sure this doesn't invalidate unless really needed.
|
||||
const selectArgs = useMemo(
|
||||
() => ({
|
||||
hasSession,
|
||||
savedFeeds: preferences?.savedFeeds || [],
|
||||
moderationOpts,
|
||||
}),
|
||||
[hasSession, preferences?.savedFeeds],
|
||||
[hasSession, preferences?.savedFeeds, moderationOpts],
|
||||
)
|
||||
const lastPageCountRef = useRef(0)
|
||||
|
||||
@@ -225,6 +229,7 @@ export function useGetPopularFeedsQuery(options?: GetPopularFeedsOptions) {
|
||||
QueryKey,
|
||||
string | undefined
|
||||
>({
|
||||
enabled: Boolean(moderationOpts),
|
||||
queryKey: createGetPopularFeedsQueryKey(options),
|
||||
queryFn: async ({pageParam}) => {
|
||||
const res = await agent.app.bsky.unspecced.getPopularFeedGenerators({
|
||||
@@ -246,7 +251,11 @@ export function useGetPopularFeedsQuery(options?: GetPopularFeedsOptions) {
|
||||
(
|
||||
data: InfiniteData<AppBskyUnspeccedGetPopularFeedGenerators.OutputSchema>,
|
||||
) => {
|
||||
const {savedFeeds, hasSession: hasSessionInner} = selectArgs
|
||||
const {
|
||||
savedFeeds,
|
||||
hasSession: hasSessionInner,
|
||||
moderationOpts,
|
||||
} = selectArgs
|
||||
return {
|
||||
...data,
|
||||
pages: data.pages.map(page => {
|
||||
@@ -264,7 +273,8 @@ export function useGetPopularFeedsQuery(options?: GetPopularFeedsOptions) {
|
||||
return f.value === feed.uri
|
||||
}),
|
||||
)
|
||||
return !alreadySaved
|
||||
const decision = moderateFeedGenerator(feed, moderationOpts!)
|
||||
return !alreadySaved && !decision.ui('contentList').filter
|
||||
}),
|
||||
}
|
||||
}),
|
||||
@@ -304,6 +314,8 @@ export function useGetPopularFeedsQuery(options?: GetPopularFeedsOptions) {
|
||||
|
||||
export function useSearchPopularFeedsMutation() {
|
||||
const agent = useAgent()
|
||||
const moderationOpts = useModerationOpts()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (query: string) => {
|
||||
const res = await agent.app.bsky.unspecced.getPopularFeedGenerators({
|
||||
@@ -311,24 +323,15 @@ export function useSearchPopularFeedsMutation() {
|
||||
query: query,
|
||||
})
|
||||
|
||||
return res.data.feeds
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useSearchPopularFeedsQuery({q}: {q: string}) {
|
||||
const agent = useAgent()
|
||||
return useQuery({
|
||||
queryKey: ['searchPopularFeeds', q],
|
||||
queryFn: async () => {
|
||||
const res = await agent.app.bsky.unspecced.getPopularFeedGenerators({
|
||||
limit: 15,
|
||||
query: q,
|
||||
})
|
||||
if (moderationOpts) {
|
||||
return res.data.feeds.filter(feed => {
|
||||
const decision = moderateFeedGenerator(feed, moderationOpts)
|
||||
return !decision.ui('contentList').filter
|
||||
})
|
||||
}
|
||||
|
||||
return res.data.feeds
|
||||
},
|
||||
placeholderData: keepPreviousData,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -346,17 +349,27 @@ export function usePopularFeedsSearch({
|
||||
enabled?: boolean
|
||||
}) {
|
||||
const agent = useAgent()
|
||||
const moderationOpts = useModerationOpts()
|
||||
const enabledInner = enabled ?? Boolean(moderationOpts)
|
||||
|
||||
return useQuery({
|
||||
enabled,
|
||||
enabled: enabledInner,
|
||||
queryKey: createPopularFeedsSearchQueryKey(query),
|
||||
queryFn: async () => {
|
||||
const res = await agent.app.bsky.unspecced.getPopularFeedGenerators({
|
||||
limit: 10,
|
||||
limit: 15,
|
||||
query: query,
|
||||
})
|
||||
|
||||
return res.data.feeds
|
||||
},
|
||||
placeholderData: keepPreviousData,
|
||||
select(data) {
|
||||
return data.filter(feed => {
|
||||
const decision = moderateFeedGenerator(feed, moderationOpts!)
|
||||
return !decision.ui('contentList').filter
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -59,7 +59,6 @@ export function useNotificationFeedQuery(opts?: {
|
||||
const moderationOpts = useModerationOpts()
|
||||
const unreads = useUnreadNotificationsApi()
|
||||
const enabled = opts?.enabled !== false
|
||||
const lastPageCountRef = useRef(0)
|
||||
const gate = useGate()
|
||||
|
||||
// false: force showing all notifications
|
||||
@@ -121,28 +120,52 @@ export function useNotificationFeedQuery(opts?: {
|
||||
},
|
||||
})
|
||||
|
||||
// The server may end up returning an empty page, a page with too few items,
|
||||
// or a page with items that end up getting filtered out. When we fetch pages,
|
||||
// we'll keep track of how many items we actually hope to see. If the server
|
||||
// doesn't return enough items, we're going to continue asking for more items.
|
||||
const lastItemCount = useRef(0)
|
||||
const wantedItemCount = useRef(0)
|
||||
const autoPaginationAttemptCount = useRef(0)
|
||||
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
|
||||
const {data, isLoading, isRefetching, isFetchingNextPage, hasNextPage} =
|
||||
query
|
||||
// Count the items that we already have.
|
||||
let itemCount = 0
|
||||
for (const page of data?.pages || []) {
|
||||
count += page.items.length
|
||||
itemCount += page.items.length
|
||||
}
|
||||
if (count < PAGE_SIZE && (data?.pages.length || 0) < 6) {
|
||||
query.fetchNextPage()
|
||||
lastPageCountRef.current = data?.pages?.length || 0
|
||||
|
||||
// If items got truncated, reset the state we're tracking below.
|
||||
if (itemCount !== lastItemCount.current) {
|
||||
if (itemCount < lastItemCount.current) {
|
||||
wantedItemCount.current = itemCount
|
||||
}
|
||||
lastItemCount.current = itemCount
|
||||
}
|
||||
|
||||
// Now track how many items we really want, and fetch more if needed.
|
||||
if (isLoading || isRefetching) {
|
||||
// During the initial fetch, we want to get an entire page's worth of items.
|
||||
wantedItemCount.current = PAGE_SIZE
|
||||
} else if (isFetchingNextPage) {
|
||||
if (itemCount > wantedItemCount.current) {
|
||||
// We have more items than wantedItemCount, so wantedItemCount must be out of date.
|
||||
// Some other code must have called fetchNextPage(), for example, from onEndReached.
|
||||
// Adjust the wantedItemCount to reflect that we want one more full page of items.
|
||||
wantedItemCount.current = itemCount + PAGE_SIZE
|
||||
}
|
||||
} else if (hasNextPage) {
|
||||
// At this point we're not fetching anymore, so it's time to make a decision.
|
||||
// If we didn't receive enough items from the server, paginate again until we do.
|
||||
if (itemCount < wantedItemCount.current) {
|
||||
autoPaginationAttemptCount.current++
|
||||
if (autoPaginationAttemptCount.current < 50 /* failsafe */) {
|
||||
query.fetchNextPage()
|
||||
}
|
||||
} else {
|
||||
autoPaginationAttemptCount.current = 0
|
||||
}
|
||||
}
|
||||
}, [query])
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ import {LikesFeedAPI} from 'lib/api/feed/likes'
|
||||
import {ListFeedAPI} from 'lib/api/feed/list'
|
||||
import {MergeFeedAPI} from 'lib/api/feed/merge'
|
||||
import {FeedAPI, ReasonFeedSource} from 'lib/api/feed/types'
|
||||
import {FeedTuner, FeedTunerFn, NoopFeedTuner} from 'lib/api/feed-manip'
|
||||
import {FeedTuner, FeedTunerFn} from 'lib/api/feed-manip'
|
||||
import {BSKY_FEED_OWNER_DIDS} from 'lib/constants'
|
||||
import {KnownError} from '#/view/com/posts/FeedErrorMessage'
|
||||
import {useFeedTuners} from '../preferences/feed-tuners'
|
||||
@@ -61,7 +61,6 @@ export type FeedDescriptor =
|
||||
| `list|${ListUri}`
|
||||
| `list|${ListUri}|${ListFilter}`
|
||||
export interface FeedParams {
|
||||
disableTuner?: boolean
|
||||
mergeFeedEnabled?: boolean
|
||||
mergeFeedSources?: string[]
|
||||
}
|
||||
@@ -105,7 +104,7 @@ export interface FeedPageUnselected {
|
||||
|
||||
export interface FeedPage {
|
||||
api: FeedAPI
|
||||
tuner: FeedTuner | NoopFeedTuner
|
||||
tuner: FeedTuner
|
||||
cursor: string | undefined
|
||||
slices: FeedPostSlice[]
|
||||
fetchedAt: number
|
||||
@@ -135,25 +134,17 @@ export function usePostFeedQuery(
|
||||
args: typeof selectArgs
|
||||
result: InfiniteData<FeedPage>
|
||||
} | null>(null)
|
||||
const lastPageCountRef = useRef(0)
|
||||
const isDiscover = feedDesc.includes(DISCOVER_FEED_URI)
|
||||
|
||||
// Make sure this doesn't invalidate unless really needed.
|
||||
const selectArgs = React.useMemo(
|
||||
() => ({
|
||||
feedTuners,
|
||||
disableTuner: params?.disableTuner,
|
||||
moderationOpts,
|
||||
ignoreFilterFor: opts?.ignoreFilterFor,
|
||||
isDiscover,
|
||||
}),
|
||||
[
|
||||
feedTuners,
|
||||
params?.disableTuner,
|
||||
moderationOpts,
|
||||
opts?.ignoreFilterFor,
|
||||
isDiscover,
|
||||
],
|
||||
[feedTuners, moderationOpts, opts?.ignoreFilterFor, isDiscover],
|
||||
)
|
||||
|
||||
const query = useInfiniteQuery<
|
||||
@@ -232,17 +223,10 @@ export function usePostFeedQuery(
|
||||
(data: InfiniteData<FeedPageUnselected, RQPageParam>) => {
|
||||
// If the selection depends on some data, that data should
|
||||
// be included in the selectArgs object and read here.
|
||||
const {
|
||||
feedTuners,
|
||||
disableTuner,
|
||||
moderationOpts,
|
||||
ignoreFilterFor,
|
||||
isDiscover,
|
||||
} = selectArgs
|
||||
const {feedTuners, moderationOpts, ignoreFilterFor, isDiscover} =
|
||||
selectArgs
|
||||
|
||||
const tuner = disableTuner
|
||||
? new NoopFeedTuner()
|
||||
: new FeedTuner(feedTuners)
|
||||
const tuner = new FeedTuner(feedTuners)
|
||||
|
||||
// Keep track of the last run and whether we can reuse
|
||||
// some already selected pages from there.
|
||||
@@ -391,30 +375,54 @@ export function usePostFeedQuery(
|
||||
),
|
||||
})
|
||||
|
||||
// The server may end up returning an empty page, a page with too few items,
|
||||
// or a page with items that end up getting filtered out. When we fetch pages,
|
||||
// we'll keep track of how many items we actually hope to see. If the server
|
||||
// doesn't return enough items, we're going to continue asking for more items.
|
||||
const lastItemCount = useRef(0)
|
||||
const wantedItemCount = useRef(0)
|
||||
const autoPaginationAttemptCount = useRef(0)
|
||||
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
|
||||
const {data, isLoading, isRefetching, isFetchingNextPage, hasNextPage} =
|
||||
query
|
||||
// Count the items that we already have.
|
||||
let itemCount = 0
|
||||
for (const page of data?.pages || []) {
|
||||
for (const slice of page.slices) {
|
||||
count += slice.items.length
|
||||
itemCount += slice.items.length
|
||||
}
|
||||
}
|
||||
if (count < PAGE_SIZE && (data?.pages.length || 0) < 6) {
|
||||
query.fetchNextPage()
|
||||
lastPageCountRef.current = data?.pages?.length || 0
|
||||
|
||||
// If items got truncated, reset the state we're tracking below.
|
||||
if (itemCount !== lastItemCount.current) {
|
||||
if (itemCount < lastItemCount.current) {
|
||||
wantedItemCount.current = itemCount
|
||||
}
|
||||
lastItemCount.current = itemCount
|
||||
}
|
||||
|
||||
// Now track how many items we really want, and fetch more if needed.
|
||||
if (isLoading || isRefetching) {
|
||||
// During the initial fetch, we want to get an entire page's worth of items.
|
||||
wantedItemCount.current = PAGE_SIZE
|
||||
} else if (isFetchingNextPage) {
|
||||
if (itemCount > wantedItemCount.current) {
|
||||
// We have more items than wantedItemCount, so wantedItemCount must be out of date.
|
||||
// Some other code must have called fetchNextPage(), for example, from onEndReached.
|
||||
// Adjust the wantedItemCount to reflect that we want one more full page of items.
|
||||
wantedItemCount.current = itemCount + PAGE_SIZE
|
||||
}
|
||||
} else if (hasNextPage) {
|
||||
// At this point we're not fetching anymore, so it's time to make a decision.
|
||||
// If we didn't receive enough items from the server, paginate again until we do.
|
||||
if (itemCount < wantedItemCount.current) {
|
||||
autoPaginationAttemptCount.current++
|
||||
if (autoPaginationAttemptCount.current < 50 /* failsafe */) {
|
||||
query.fetchNextPage()
|
||||
}
|
||||
} else {
|
||||
autoPaginationAttemptCount.current = 0
|
||||
}
|
||||
}
|
||||
}, [query])
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ import {
|
||||
export const DEFAULT_HOME_FEED_PREFS: UsePreferencesQueryResponse['feedViewPrefs'] =
|
||||
{
|
||||
hideReplies: false,
|
||||
hideRepliesByUnfollowed: true,
|
||||
hideRepliesByLikeCount: 0,
|
||||
hideRepliesByUnfollowed: true, // Legacy, ignored
|
||||
hideRepliesByLikeCount: 0, // Legacy, ignored
|
||||
hideReposts: false,
|
||||
hideQuotePosts: false,
|
||||
lab_mergeFeedEnabled: false, // experimental
|
||||
|
||||
@@ -343,6 +343,21 @@ export function useRemoveMutedWordMutation() {
|
||||
})
|
||||
}
|
||||
|
||||
export function useRemoveMutedWordsMutation() {
|
||||
const queryClient = useQueryClient()
|
||||
const agent = useAgent()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (mutedWords: AppBskyActorDefs.MutedWord[]) => {
|
||||
await agent.removeMutedWords(mutedWords)
|
||||
// triggers a refetch
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: preferencesQueryKey,
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useQueueNudgesMutation() {
|
||||
const queryClient = useQueryClient()
|
||||
const agent = useAgent()
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import {AppBskyFeedGetActorFeeds} from '@atproto/api'
|
||||
import {AppBskyFeedGetActorFeeds, moderateFeedGenerator} from '@atproto/api'
|
||||
import {InfiniteData, QueryKey, useInfiniteQuery} from '@tanstack/react-query'
|
||||
|
||||
import {useAgent} from '#/state/session'
|
||||
import {useModerationOpts} from '../preferences/moderation-opts'
|
||||
|
||||
const PAGE_SIZE = 50
|
||||
type RQPageParam = string | undefined
|
||||
@@ -14,7 +15,8 @@ export function useProfileFeedgensQuery(
|
||||
did: string,
|
||||
opts?: {enabled?: boolean},
|
||||
) {
|
||||
const enabled = opts?.enabled !== false
|
||||
const moderationOpts = useModerationOpts()
|
||||
const enabled = opts?.enabled !== false && Boolean(moderationOpts)
|
||||
const agent = useAgent()
|
||||
return useInfiniteQuery<
|
||||
AppBskyFeedGetActorFeeds.OutputSchema,
|
||||
@@ -38,5 +40,21 @@ export function useProfileFeedgensQuery(
|
||||
initialPageParam: undefined,
|
||||
getNextPageParam: lastPage => lastPage.cursor,
|
||||
enabled,
|
||||
select(data) {
|
||||
return {
|
||||
...data,
|
||||
pages: data.pages.map(page => {
|
||||
return {
|
||||
...page,
|
||||
feeds: page.feeds
|
||||
// filter by labels
|
||||
.filter(list => {
|
||||
const decision = moderateFeedGenerator(list, moderationOpts!)
|
||||
return !decision.ui('contentList').filter
|
||||
}),
|
||||
}
|
||||
}),
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import {AppBskyGraphGetLists} from '@atproto/api'
|
||||
import {AppBskyGraphGetLists, moderateUserList} from '@atproto/api'
|
||||
import {InfiniteData, QueryKey, useInfiniteQuery} from '@tanstack/react-query'
|
||||
|
||||
import {useAgent} from '#/state/session'
|
||||
import {useModerationOpts} from '../preferences/moderation-opts'
|
||||
|
||||
const PAGE_SIZE = 30
|
||||
type RQPageParam = string | undefined
|
||||
@@ -10,7 +11,8 @@ const RQKEY_ROOT = 'profile-lists'
|
||||
export const RQKEY = (did: string) => [RQKEY_ROOT, did]
|
||||
|
||||
export function useProfileListsQuery(did: string, opts?: {enabled?: boolean}) {
|
||||
const enabled = opts?.enabled !== false
|
||||
const moderationOpts = useModerationOpts()
|
||||
const enabled = opts?.enabled !== false && Boolean(moderationOpts)
|
||||
const agent = useAgent()
|
||||
return useInfiniteQuery<
|
||||
AppBskyGraphGetLists.OutputSchema,
|
||||
@@ -27,17 +29,32 @@ export function useProfileListsQuery(did: string, opts?: {enabled?: boolean}) {
|
||||
cursor: pageParam,
|
||||
})
|
||||
|
||||
// Starter packs use a reference list, which we do not want to show on profiles. At some point we could probably
|
||||
// just filter this out on the backend instead of in the client.
|
||||
return {
|
||||
...res.data,
|
||||
lists: res.data.lists.filter(
|
||||
l => l.purpose !== 'app.bsky.graph.defs#referencelist',
|
||||
),
|
||||
}
|
||||
return res.data
|
||||
},
|
||||
initialPageParam: undefined,
|
||||
getNextPageParam: lastPage => lastPage.cursor,
|
||||
enabled,
|
||||
select(data) {
|
||||
return {
|
||||
...data,
|
||||
pages: data.pages.map(page => {
|
||||
return {
|
||||
...page,
|
||||
lists: page.lists
|
||||
/*
|
||||
* Starter packs use a reference list, which we do not want to
|
||||
* show on profiles. At some point we could probably just filter
|
||||
* this out on the backend instead of in the client.
|
||||
*/
|
||||
.filter(l => l.purpose !== 'app.bsky.graph.defs#referencelist')
|
||||
// filter by labels
|
||||
.filter(list => {
|
||||
const decision = moderateUserList(list, moderationOpts!)
|
||||
return !decision.ui('contentList').filter
|
||||
}),
|
||||
}
|
||||
}),
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user