diff options
Diffstat (limited to 'src/state/queries')
-rw-r--r-- | src/state/queries/feed.ts | 129 | ||||
-rw-r--r-- | src/state/queries/post-feed.ts | 61 | ||||
-rw-r--r-- | src/state/queries/preferences/const.ts | 2 | ||||
-rw-r--r-- | src/state/queries/preferences/index.ts | 49 |
4 files changed, 143 insertions, 98 deletions
diff --git a/src/state/queries/feed.ts b/src/state/queries/feed.ts index 67294ece2..1fa92c291 100644 --- a/src/state/queries/feed.ts +++ b/src/state/queries/feed.ts @@ -1,11 +1,9 @@ -import React from 'react' import { useQuery, useInfiniteQuery, InfiniteData, QueryKey, useMutation, - useQueryClient, } from '@tanstack/react-query' import { AtUri, @@ -15,7 +13,6 @@ import { AppBskyUnspeccedGetPopularFeedGenerators, } from '@atproto/api' -import {logger} from '#/logger' import {router} from '#/routes' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' @@ -219,83 +216,59 @@ const FOLLOWING_FEED_STUB: FeedSourceInfo = { likeUri: '', } -export function usePinnedFeedsInfos(): { - feeds: FeedSourceInfo[] - hasPinnedCustom: boolean - isLoading: boolean -} { - const queryClient = useQueryClient() - const [tabs, setTabs] = React.useState<FeedSourceInfo[]>([ - FOLLOWING_FEED_STUB, - ]) - const [isLoading, setLoading] = React.useState(true) - const {data: preferences} = usePreferencesQuery() +export function usePinnedFeedsInfos() { + const {data: preferences, isLoading: isLoadingPrefs} = usePreferencesQuery() + const pinnedUris = preferences?.feeds?.pinned ?? [] - const hasPinnedCustom = React.useMemo<boolean>(() => { - return tabs.some(tab => tab !== FOLLOWING_FEED_STUB) - }, [tabs]) - - React.useEffect(() => { - if (!preferences?.feeds?.pinned) return - const uris = preferences.feeds.pinned - - async function fetchFeedInfo() { - const reqs = [] - - for (const uri of uris) { - const cached = queryClient.getQueryData<FeedSourceInfo>( - feedSourceInfoQueryKey({uri}), - ) - - if (cached) { - reqs.push(cached) - } else { - reqs.push( - (async () => { - // these requests can fail, need to filter those out - try { - return await queryClient.fetchQuery({ - staleTime: STALE.SECONDS.FIFTEEN, - queryKey: feedSourceInfoQueryKey({uri}), - queryFn: async () => { - const type = getFeedTypeFromUri(uri) + return useQuery({ + staleTime: STALE.INFINITY, + enabled: !isLoadingPrefs, + queryKey: ['pinnedFeedsInfos', pinnedUris.join(',')], + queryFn: async () => { + let resolved = new Map() + + // Get all feeds. We can do this in a batch. + const feedUris = pinnedUris.filter( + uri => getFeedTypeFromUri(uri) === 'feed', + ) + let feedsPromise = Promise.resolve() + if (feedUris.length > 0) { + feedsPromise = getAgent() + .app.bsky.feed.getFeedGenerators({ + feeds: feedUris, + }) + .then(res => { + for (let feedView of res.data.feeds) { + resolved.set(feedView.uri, hydrateFeedGenerator(feedView)) + } + }) + } - if (type === 'feed') { - const res = - await getAgent().app.bsky.feed.getFeedGenerator({ - feed: uri, - }) - return hydrateFeedGenerator(res.data.view) - } else { - const res = await getAgent().app.bsky.graph.getList({ - list: uri, - limit: 1, - }) - return hydrateList(res.data.list) - } - }, - }) - } catch (e) { - // expected failure - logger.info(`usePinnedFeedsInfos: failed to fetch ${uri}`, { - error: e, - }) - } - })(), - ) + // Get all lists. This currently has to be done individually. + const listUris = pinnedUris.filter( + uri => getFeedTypeFromUri(uri) === 'list', + ) + const listsPromises = listUris.map(listUri => + getAgent() + .app.bsky.graph.getList({ + list: listUri, + limit: 1, + }) + .then(res => { + const listView = res.data.list + resolved.set(listView.uri, hydrateList(listView)) + }), + ) + + // The returned result will have the original order. + const result = [FOLLOWING_FEED_STUB] + await Promise.allSettled([feedsPromise, ...listsPromises]) + for (let pinnedUri of pinnedUris) { + if (resolved.has(pinnedUri)) { + result.push(resolved.get(pinnedUri)) } } - - const views = (await Promise.all(reqs)).filter( - Boolean, - ) as FeedSourceInfo[] - - setTabs([FOLLOWING_FEED_STUB].concat(views)) - setLoading(false) - } - - fetchFeedInfo() - }, [queryClient, setTabs, preferences?.feeds?.pinned]) - - return {feeds: tabs, hasPinnedCustom, isLoading} + return result + }, + }) } diff --git a/src/state/queries/post-feed.ts b/src/state/queries/post-feed.ts index 40399395a..c295ffcb0 100644 --- a/src/state/queries/post-feed.ts +++ b/src/state/queries/post-feed.ts @@ -1,6 +1,11 @@ import React, {useCallback, useEffect, useRef} from 'react' import {AppState} from 'react-native' -import {AppBskyFeedDefs, AppBskyFeedPost, PostModeration} from '@atproto/api' +import { + AppBskyFeedDefs, + AppBskyFeedPost, + AtUri, + PostModeration, +} from '@atproto/api' import { useInfiniteQuery, InfiniteData, @@ -29,6 +34,7 @@ import {KnownError} from '#/view/com/posts/FeedErrorMessage' import {embedViewRecordToPostView, getEmbeddedPost} from './util' import {useModerationOpts} from './preferences' import {queryClient} from 'lib/react-query' +import {BSKY_FEED_OWNER_DIDS} from 'lib/constants' type ActorDid = string type AuthorFilter = @@ -137,24 +143,41 @@ export function usePostFeedQuery( cursor: undefined, } - const res = await api.fetch({cursor, limit: PAGE_SIZE}) - precacheFeedPostProfiles(queryClient, res.feed) - - /* - * If this is a public view, we need to check if posts fail moderation. - * If all fail, we throw an error. If only some fail, we continue and let - * moderations happen later, which results in some posts being shown and - * some not. - */ - if (!getAgent().session) { - assertSomePostsPassModeration(res.feed) - } + try { + const res = await api.fetch({cursor, limit: PAGE_SIZE}) + precacheFeedPostProfiles(queryClient, res.feed) + + /* + * If this is a public view, we need to check if posts fail moderation. + * If all fail, we throw an error. If only some fail, we continue and let + * moderations happen later, which results in some posts being shown and + * some not. + */ + if (!getAgent().session) { + assertSomePostsPassModeration(res.feed) + } + + return { + api, + cursor: res.cursor, + feed: res.feed, + fetchedAt: Date.now(), + } + } catch (e) { + const feedDescParts = feedDesc.split('|') + const feedOwnerDid = new AtUri(feedDescParts[1]).hostname + + if ( + feedDescParts[0] === 'feedgen' && + BSKY_FEED_OWNER_DIDS.includes(feedOwnerDid) + ) { + logger.error(`Bluesky feed may be offline: ${feedOwnerDid}`, { + feedDesc, + jsError: e, + }) + } - return { - api, - cursor: res.cursor, - feed: res.feed, - fetchedAt: Date.now(), + throw e } }, initialPageParam: undefined, @@ -253,7 +276,7 @@ export function usePostFeedQuery( .success ) { return { - _reactKey: `${slice._reactKey}-${i}`, + _reactKey: `${slice._reactKey}-${i}-${item.post.uri}`, uri: item.post.uri, post: item.post, record: item.post.record, diff --git a/src/state/queries/preferences/const.ts b/src/state/queries/preferences/const.ts index 2d9d02994..25d284998 100644 --- a/src/state/queries/preferences/const.ts +++ b/src/state/queries/preferences/const.ts @@ -49,4 +49,6 @@ export const DEFAULT_LOGGED_OUT_PREFERENCES: UsePreferencesQueryResponse = { threadViewPrefs: DEFAULT_THREAD_VIEW_PREFS, userAge: 13, // TODO(pwi) interests: {tags: []}, + mutedWords: [], + hiddenPosts: [], } diff --git a/src/state/queries/preferences/index.ts b/src/state/queries/preferences/index.ts index 632d31a13..07198de77 100644 --- a/src/state/queries/preferences/index.ts +++ b/src/state/queries/preferences/index.ts @@ -1,6 +1,10 @@ import {useMemo} from 'react' import {useQuery, useMutation, useQueryClient} from '@tanstack/react-query' -import {LabelPreference, BskyFeedViewPreference} from '@atproto/api' +import { + LabelPreference, + BskyFeedViewPreference, + AppBskyActorDefs, +} from '@atproto/api' import {track} from '#/lib/analytics/analytics' import {getAge} from '#/lib/strings/time' @@ -108,6 +112,7 @@ export function useModerationOpts() { return { ...moderationOpts, hiddenPosts, + mutedWords: prefs.data.mutedWords || [], } }, [currentAccount?.did, prefs.data, hiddenPosts]) return opts @@ -278,3 +283,45 @@ export function useUnpinFeedMutation() { }, }) } + +export function useUpsertMutedWordsMutation() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async (mutedWords: AppBskyActorDefs.MutedWord[]) => { + await getAgent().upsertMutedWords(mutedWords) + // triggers a refetch + await queryClient.invalidateQueries({ + queryKey: preferencesQueryKey, + }) + }, + }) +} + +export function useUpdateMutedWordMutation() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async (mutedWord: AppBskyActorDefs.MutedWord) => { + await getAgent().updateMutedWord(mutedWord) + // triggers a refetch + await queryClient.invalidateQueries({ + queryKey: preferencesQueryKey, + }) + }, + }) +} + +export function useRemoveMutedWordMutation() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async (mutedWord: AppBskyActorDefs.MutedWord) => { + await getAgent().removeMutedWord(mutedWord) + // triggers a refetch + await queryClient.invalidateQueries({ + queryKey: preferencesQueryKey, + }) + }, + }) +} |