[Explore] New suggested follows endpoint (#8130)
* Bump SDK * Integrate new endpoint, add profile shadow, For You tab * Format
This commit is contained in:
+1
-1
@@ -58,7 +58,7 @@
|
||||
"icons:optimize": "svgo -f ./assets/icons"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "^0.14.19",
|
||||
"@atproto/api": "^0.14.20",
|
||||
"@bitdrift/react-native": "^0.6.8",
|
||||
"@braintree/sanitize-url": "^6.0.2",
|
||||
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
||||
|
||||
+29
-100
@@ -15,7 +15,6 @@ import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {logger} from '#/logger'
|
||||
import {type MetricEvents} from '#/logger/metrics'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {useActorSearchPaginated} from '#/state/queries/actor-search'
|
||||
import {
|
||||
type FeedPreviewItem,
|
||||
useFeedPreviews,
|
||||
@@ -23,8 +22,8 @@ import {
|
||||
import {useGetPopularFeedsQuery} from '#/state/queries/feed'
|
||||
import {Nux, useNux} from '#/state/queries/nuxs'
|
||||
import {usePreferencesQuery} from '#/state/queries/preferences'
|
||||
import {useSuggestedFollowsQuery} from '#/state/queries/suggested-follows'
|
||||
import {useGetSuggestedFeedsQuery} from '#/state/queries/trending/useGetSuggestedFeedsQuery'
|
||||
import {useGetSuggestedUsersQuery} from '#/state/queries/trending/useGetSuggestedUsersQuery'
|
||||
import {useSuggestedStarterPacksQuery} from '#/state/queries/useSuggestedStarterPacksQuery'
|
||||
import {useProgressGuide} from '#/state/shell/progress-guide'
|
||||
import {isThreadChildAt, isThreadParentAt} from '#/view/com/posts/PostFeed'
|
||||
@@ -57,7 +56,6 @@ import * as ModuleHeader from './components/ModuleHeader'
|
||||
import {
|
||||
SuggestedAccountsTabBar,
|
||||
SuggestedProfileCard,
|
||||
useLoadEnoughProfiles,
|
||||
} from './modules/ExploreSuggestedAccounts'
|
||||
|
||||
function LoadMore({item}: {item: ExploreScreenItems & {type: 'loadMore'}}) {
|
||||
@@ -144,7 +142,7 @@ type ExploreScreenItems =
|
||||
| {
|
||||
type: 'profile'
|
||||
key: string
|
||||
profile: AppBskyActorDefs.ProfileView
|
||||
profile: AppBskyActorDefs.ProfileViewBasic
|
||||
recId?: number
|
||||
}
|
||||
| {
|
||||
@@ -203,33 +201,13 @@ export function Explore({
|
||||
const gate = useGate()
|
||||
const guide = useProgressGuide('follow-10')
|
||||
const [selectedInterest, setSelectedInterest] = useState<string | null>(null)
|
||||
// TODO always get at least 10 back
|
||||
const {
|
||||
data: suggestedProfiles,
|
||||
hasNextPage: hasNextSuggestedProfilesPage,
|
||||
isLoading: isLoadingSuggestedProfiles,
|
||||
isFetchingNextPage: isFetchingNextSuggestedProfilesPage,
|
||||
error: suggestedProfilesError,
|
||||
fetchNextPage: fetchNextSuggestedProfilesPage,
|
||||
} = useSuggestedFollowsQuery({limit: 3, subsequentPageLimit: 10})
|
||||
const {
|
||||
data: interestProfiles,
|
||||
hasNextPage: hasNextInterestProfilesPage,
|
||||
isLoading: isLoadingInterestProfiles,
|
||||
isFetchingNextPage: isFetchingNextInterestProfilesPage,
|
||||
error: interestProfilesError,
|
||||
fetchNextPage: fetchNextInterestProfilesPage,
|
||||
} = useActorSearchPaginated({
|
||||
query: selectedInterest || '',
|
||||
enabled: !!selectedInterest,
|
||||
limit: 10,
|
||||
})
|
||||
const {isReady: canShowSuggestedProfiles} = useLoadEnoughProfiles({
|
||||
interest: selectedInterest,
|
||||
data: interestProfiles,
|
||||
isLoading: isLoadingInterestProfiles,
|
||||
isFetchingNextPage: isFetchingNextInterestProfilesPage,
|
||||
hasNextPage: hasNextInterestProfilesPage,
|
||||
fetchNextPage: fetchNextInterestProfilesPage,
|
||||
data: suggestedUsers,
|
||||
isLoading: suggestedUsersIsLoading,
|
||||
error: suggestedUsersError,
|
||||
} = useGetSuggestedUsersQuery({
|
||||
category: selectedInterest,
|
||||
})
|
||||
const {
|
||||
data: feeds,
|
||||
@@ -243,39 +221,6 @@ export function Explore({
|
||||
const showInterestsNux =
|
||||
interestsNux.status === 'ready' && !interestsNux.nux?.completed
|
||||
|
||||
const profiles: typeof suggestedProfiles & typeof interestProfiles =
|
||||
!selectedInterest ? suggestedProfiles : interestProfiles
|
||||
const hasNextProfilesPage = !selectedInterest
|
||||
? hasNextSuggestedProfilesPage
|
||||
: hasNextInterestProfilesPage
|
||||
const isLoadingProfiles = !selectedInterest
|
||||
? isLoadingSuggestedProfiles
|
||||
: !canShowSuggestedProfiles
|
||||
const isFetchingNextProfilesPage = !selectedInterest
|
||||
? isFetchingNextSuggestedProfilesPage
|
||||
: !canShowSuggestedProfiles
|
||||
const profilesError = !selectedInterest
|
||||
? suggestedProfilesError
|
||||
: interestProfilesError
|
||||
const fetchNextProfilesPage = !selectedInterest
|
||||
? fetchNextSuggestedProfilesPage
|
||||
: fetchNextInterestProfilesPage
|
||||
|
||||
const isLoadingMoreProfiles = isFetchingNextProfilesPage && !isLoadingProfiles
|
||||
const onLoadMoreProfiles = useCallback(async () => {
|
||||
if (isFetchingNextProfilesPage || !hasNextProfilesPage || profilesError)
|
||||
return
|
||||
try {
|
||||
await fetchNextProfilesPage()
|
||||
} catch (err) {
|
||||
logger.error('Failed to load more suggested follows', {message: err})
|
||||
}
|
||||
}, [
|
||||
isFetchingNextProfilesPage,
|
||||
hasNextProfilesPage,
|
||||
profilesError,
|
||||
fetchNextProfilesPage,
|
||||
])
|
||||
const {
|
||||
data: suggestedSPs,
|
||||
isLoading: isLoadingSuggestedSPs,
|
||||
@@ -358,55 +303,42 @@ export function Explore({
|
||||
},
|
||||
})
|
||||
|
||||
if (!canShowSuggestedProfiles) {
|
||||
if (suggestedUsersIsLoading) {
|
||||
i.push({type: 'profilePlaceholder', key: 'profilePlaceholder'})
|
||||
} else if (profilesError) {
|
||||
} else if (suggestedUsersError) {
|
||||
i.push({
|
||||
type: 'error',
|
||||
key: 'profilesError',
|
||||
key: 'suggestedUsersError',
|
||||
message: _(msg`Failed to load suggested follows`),
|
||||
error: cleanError(profilesError),
|
||||
error: cleanError(suggestedUsersError),
|
||||
})
|
||||
} else {
|
||||
if (profiles !== undefined) {
|
||||
if (profiles.pages.length > 0 && moderationOpts) {
|
||||
if (suggestedUsers !== undefined) {
|
||||
if (suggestedUsers.actors.length > 0 && moderationOpts) {
|
||||
// Currently the responses contain duplicate items.
|
||||
// Needs to be fixed on backend, but let's dedupe to be safe.
|
||||
let seen = new Set()
|
||||
const profileItems: ExploreScreenItems[] = []
|
||||
for (const page of profiles.pages) {
|
||||
for (const actor of page.actors) {
|
||||
if (!seen.has(actor.did) && !actor.viewer?.following) {
|
||||
seen.add(actor.did)
|
||||
profileItems.push({
|
||||
type: 'profile',
|
||||
key: actor.did,
|
||||
profile: actor,
|
||||
recId: page.recId,
|
||||
})
|
||||
}
|
||||
for (const actor of suggestedUsers.actors) {
|
||||
if (!seen.has(actor.did) && !actor.viewer?.following) {
|
||||
seen.add(actor.did)
|
||||
profileItems.push({
|
||||
type: 'profile',
|
||||
key: actor.did,
|
||||
profile: actor,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (profileItems.length === 0) {
|
||||
if (!hasNextProfilesPage) {
|
||||
// no items! remove the header
|
||||
i.pop()
|
||||
}
|
||||
// no items! remove the header
|
||||
i.pop()
|
||||
} else {
|
||||
i.push(...profileItems)
|
||||
}
|
||||
if (hasNextProfilesPage) {
|
||||
i.push({
|
||||
type: 'loadMore',
|
||||
key: 'loadMoreProfiles',
|
||||
message: _(msg`Load more suggested accounts`),
|
||||
isLoadingMore: isLoadingMoreProfiles,
|
||||
onLoadMore: onLoadMoreProfiles,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
console.log('no pages')
|
||||
// no items! remove the header
|
||||
i.pop()
|
||||
}
|
||||
} else {
|
||||
i.push({type: 'profilePlaceholder', key: 'profilePlaceholder'})
|
||||
@@ -414,14 +346,11 @@ export function Explore({
|
||||
}
|
||||
return i
|
||||
}, [
|
||||
profiles,
|
||||
_,
|
||||
canShowSuggestedProfiles,
|
||||
hasNextProfilesPage,
|
||||
isLoadingMoreProfiles,
|
||||
moderationOpts,
|
||||
onLoadMoreProfiles,
|
||||
profilesError,
|
||||
suggestedUsers,
|
||||
suggestedUsersIsLoading,
|
||||
suggestedUsersError,
|
||||
])
|
||||
const suggestedFeedsModule = useMemo(() => {
|
||||
const i: ExploreScreenItems[] = []
|
||||
|
||||
@@ -83,7 +83,7 @@ export function SuggestedAccountsTabBar({
|
||||
}}
|
||||
hasSearchText={false}
|
||||
interestsDisplayNames={{
|
||||
all: _(msg`All`),
|
||||
all: _(msg`For You`),
|
||||
...interestsDisplayNames,
|
||||
}}
|
||||
TabComponent={Tab}
|
||||
|
||||
Vendored
+2
@@ -19,6 +19,7 @@ import {findAllProfilesInQueryData as findAllProfilesInProfileQueryData} from '#
|
||||
import {findAllProfilesInQueryData as findAllProfilesInProfileFollowersQueryData} from '#/state/queries/profile-followers'
|
||||
import {findAllProfilesInQueryData as findAllProfilesInProfileFollowsQueryData} from '#/state/queries/profile-follows'
|
||||
import {findAllProfilesInQueryData as findAllProfilesInSuggestedFollowsQueryData} from '#/state/queries/suggested-follows'
|
||||
import {findAllProfilesInQueryData as findAllProfilesInSuggestedUsersQueryData} from '#/state/queries/trending/useGetSuggestedUsersQuery'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
import {castAsShadow, type Shadow} from './types'
|
||||
|
||||
@@ -149,6 +150,7 @@ function* findProfilesInCache(
|
||||
yield* findAllProfilesInProfileQueryData(queryClient, did)
|
||||
yield* findAllProfilesInProfileFollowersQueryData(queryClient, did)
|
||||
yield* findAllProfilesInProfileFollowsQueryData(queryClient, did)
|
||||
yield* findAllProfilesInSuggestedUsersQueryData(queryClient, did)
|
||||
yield* findAllProfilesInSuggestedFollowsQueryData(queryClient, did)
|
||||
yield* findAllProfilesInActorSearchQueryData(queryClient, did)
|
||||
yield* findAllProfilesInListConvosQueryData(queryClient, did)
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import {
|
||||
type AppBskyActorDefs,
|
||||
type AppBskyUnspeccedGetSuggestedUsers,
|
||||
} from '@atproto/api'
|
||||
import {type QueryClient, useQuery} from '@tanstack/react-query'
|
||||
|
||||
import {
|
||||
aggregateUserInterests,
|
||||
createBskyTopicsHeader,
|
||||
} from '#/lib/api/feed/utils'
|
||||
import {getContentLanguages} from '#/state/preferences/languages'
|
||||
import {STALE} from '#/state/queries'
|
||||
import {usePreferencesQuery} from '#/state/queries/preferences'
|
||||
import {useAgent} from '#/state/session'
|
||||
|
||||
export type QueryProps = {category?: string | null}
|
||||
|
||||
export const getSuggestedUsersQueryKeyRoot = 'unspecced-suggested-users'
|
||||
export const createGetSuggestedUsersQueryKey = (props: QueryProps) => [
|
||||
getSuggestedUsersQueryKeyRoot,
|
||||
...Object.values(props),
|
||||
]
|
||||
|
||||
export function useGetSuggestedUsersQuery(props: QueryProps) {
|
||||
const agent = useAgent()
|
||||
const {data: preferences} = usePreferencesQuery()
|
||||
|
||||
return useQuery({
|
||||
enabled: !!preferences,
|
||||
refetchOnWindowFocus: true,
|
||||
staleTime: STALE.MINUTES.ONE,
|
||||
queryKey: createGetSuggestedUsersQueryKey(props),
|
||||
queryFn: async () => {
|
||||
const contentLangs = getContentLanguages().join(',')
|
||||
const {data} = await agent.app.bsky.unspecced.getSuggestedUsers(
|
||||
{
|
||||
category: props.category ?? undefined,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
...createBskyTopicsHeader(aggregateUserInterests(preferences)),
|
||||
'Accept-Language': contentLangs,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
return data
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function* findAllProfilesInQueryData(
|
||||
queryClient: QueryClient,
|
||||
did: string,
|
||||
): Generator<AppBskyActorDefs.ProfileViewBasic, void> {
|
||||
const responses =
|
||||
queryClient.getQueriesData<AppBskyUnspeccedGetSuggestedUsers.OutputSchema>({
|
||||
queryKey: [getSuggestedUsersQueryKeyRoot],
|
||||
})
|
||||
for (const [_, response] of responses) {
|
||||
if (!response) {
|
||||
continue
|
||||
}
|
||||
|
||||
for (const actor of response.actors) {
|
||||
if (actor.did === did) {
|
||||
yield actor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,10 +80,10 @@
|
||||
tlds "^1.234.0"
|
||||
zod "^3.23.8"
|
||||
|
||||
"@atproto/api@^0.14.19":
|
||||
version "0.14.19"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.14.19.tgz#fef8994e2b14e69a9e3a0aef043c7fcb34d6bf8c"
|
||||
integrity sha512-YYTqM0K0qk2TP7PguktPzlAQGLTL1bEGz6PgY5kqKJNX4o1318kJYB22DzjJYqV2NUCq0JQ9Lb0oskLvTisEOg==
|
||||
"@atproto/api@^0.14.20":
|
||||
version "0.14.20"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.14.20.tgz#904c85a91748f3203fd929415cb8fb3bc78d35d3"
|
||||
integrity sha512-Daip22+u9N+EVPk9PsEEVrTfjIqGczXnAT7o2EHGd0JsOzMbp3a6wmW1beKqYDzPf+Dc36/39JeUYYqhB3fKjg==
|
||||
dependencies:
|
||||
"@atproto/common-web" "^0.4.1"
|
||||
"@atproto/lexicon" "^0.4.10"
|
||||
|
||||
Reference in New Issue
Block a user