Integrate new endpoint, add profile shadow, For You tab
This commit is contained in:
@@ -15,7 +15,6 @@ import {sanitizeHandle} from '#/lib/strings/handles'
|
|||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {type MetricEvents} from '#/logger/metrics'
|
import {type MetricEvents} from '#/logger/metrics'
|
||||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
import {useActorSearchPaginated} from '#/state/queries/actor-search'
|
|
||||||
import {
|
import {
|
||||||
type FeedPreviewItem,
|
type FeedPreviewItem,
|
||||||
useFeedPreviews,
|
useFeedPreviews,
|
||||||
@@ -23,8 +22,8 @@ import {
|
|||||||
import {useGetPopularFeedsQuery} from '#/state/queries/feed'
|
import {useGetPopularFeedsQuery} from '#/state/queries/feed'
|
||||||
import {Nux, useNux} from '#/state/queries/nuxs'
|
import {Nux, useNux} from '#/state/queries/nuxs'
|
||||||
import {usePreferencesQuery} from '#/state/queries/preferences'
|
import {usePreferencesQuery} from '#/state/queries/preferences'
|
||||||
import {useSuggestedFollowsQuery} from '#/state/queries/suggested-follows'
|
|
||||||
import {useGetSuggestedFeedsQuery} from '#/state/queries/trending/useGetSuggestedFeedsQuery'
|
import {useGetSuggestedFeedsQuery} from '#/state/queries/trending/useGetSuggestedFeedsQuery'
|
||||||
|
import {useGetSuggestedUsersQuery} from '#/state/queries/trending/useGetSuggestedUsersQuery'
|
||||||
import {useSuggestedStarterPacksQuery} from '#/state/queries/useSuggestedStarterPacksQuery'
|
import {useSuggestedStarterPacksQuery} from '#/state/queries/useSuggestedStarterPacksQuery'
|
||||||
import {useProgressGuide} from '#/state/shell/progress-guide'
|
import {useProgressGuide} from '#/state/shell/progress-guide'
|
||||||
import {isThreadChildAt, isThreadParentAt} from '#/view/com/posts/PostFeed'
|
import {isThreadChildAt, isThreadParentAt} from '#/view/com/posts/PostFeed'
|
||||||
@@ -57,7 +56,6 @@ import * as ModuleHeader from './components/ModuleHeader'
|
|||||||
import {
|
import {
|
||||||
SuggestedAccountsTabBar,
|
SuggestedAccountsTabBar,
|
||||||
SuggestedProfileCard,
|
SuggestedProfileCard,
|
||||||
useLoadEnoughProfiles,
|
|
||||||
} from './modules/ExploreSuggestedAccounts'
|
} from './modules/ExploreSuggestedAccounts'
|
||||||
|
|
||||||
function LoadMore({item}: {item: ExploreScreenItems & {type: 'loadMore'}}) {
|
function LoadMore({item}: {item: ExploreScreenItems & {type: 'loadMore'}}) {
|
||||||
@@ -144,7 +142,7 @@ type ExploreScreenItems =
|
|||||||
| {
|
| {
|
||||||
type: 'profile'
|
type: 'profile'
|
||||||
key: string
|
key: string
|
||||||
profile: AppBskyActorDefs.ProfileView
|
profile: AppBskyActorDefs.ProfileViewBasic
|
||||||
recId?: number
|
recId?: number
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
@@ -203,33 +201,13 @@ export function Explore({
|
|||||||
const gate = useGate()
|
const gate = useGate()
|
||||||
const guide = useProgressGuide('follow-10')
|
const guide = useProgressGuide('follow-10')
|
||||||
const [selectedInterest, setSelectedInterest] = useState<string | null>(null)
|
const [selectedInterest, setSelectedInterest] = useState<string | null>(null)
|
||||||
|
// TODO always get at least 10 back
|
||||||
const {
|
const {
|
||||||
data: suggestedProfiles,
|
data: suggestedUsers,
|
||||||
hasNextPage: hasNextSuggestedProfilesPage,
|
isLoading: suggestedUsersIsLoading,
|
||||||
isLoading: isLoadingSuggestedProfiles,
|
error: suggestedUsersError,
|
||||||
isFetchingNextPage: isFetchingNextSuggestedProfilesPage,
|
} = useGetSuggestedUsersQuery({
|
||||||
error: suggestedProfilesError,
|
category: selectedInterest,
|
||||||
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,
|
|
||||||
})
|
})
|
||||||
const {
|
const {
|
||||||
data: feeds,
|
data: feeds,
|
||||||
@@ -243,39 +221,6 @@ export function Explore({
|
|||||||
const showInterestsNux =
|
const showInterestsNux =
|
||||||
interestsNux.status === 'ready' && !interestsNux.nux?.completed
|
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 {
|
const {
|
||||||
data: suggestedSPs,
|
data: suggestedSPs,
|
||||||
isLoading: isLoadingSuggestedSPs,
|
isLoading: isLoadingSuggestedSPs,
|
||||||
@@ -358,55 +303,42 @@ export function Explore({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!canShowSuggestedProfiles) {
|
if (suggestedUsersIsLoading) {
|
||||||
i.push({type: 'profilePlaceholder', key: 'profilePlaceholder'})
|
i.push({type: 'profilePlaceholder', key: 'profilePlaceholder'})
|
||||||
} else if (profilesError) {
|
} else if (suggestedUsersError) {
|
||||||
i.push({
|
i.push({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
key: 'profilesError',
|
key: 'suggestedUsersError',
|
||||||
message: _(msg`Failed to load suggested follows`),
|
message: _(msg`Failed to load suggested follows`),
|
||||||
error: cleanError(profilesError),
|
error: cleanError(suggestedUsersError),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
if (profiles !== undefined) {
|
if (suggestedUsers !== undefined) {
|
||||||
if (profiles.pages.length > 0 && moderationOpts) {
|
if (suggestedUsers.actors.length > 0 && moderationOpts) {
|
||||||
// Currently the responses contain duplicate items.
|
// Currently the responses contain duplicate items.
|
||||||
// Needs to be fixed on backend, but let's dedupe to be safe.
|
// Needs to be fixed on backend, but let's dedupe to be safe.
|
||||||
let seen = new Set()
|
let seen = new Set()
|
||||||
const profileItems: ExploreScreenItems[] = []
|
const profileItems: ExploreScreenItems[] = []
|
||||||
for (const page of profiles.pages) {
|
for (const actor of suggestedUsers.actors) {
|
||||||
for (const actor of page.actors) {
|
|
||||||
if (!seen.has(actor.did) && !actor.viewer?.following) {
|
if (!seen.has(actor.did) && !actor.viewer?.following) {
|
||||||
seen.add(actor.did)
|
seen.add(actor.did)
|
||||||
profileItems.push({
|
profileItems.push({
|
||||||
type: 'profile',
|
type: 'profile',
|
||||||
key: actor.did,
|
key: actor.did,
|
||||||
profile: actor,
|
profile: actor,
|
||||||
recId: page.recId,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (profileItems.length === 0) {
|
if (profileItems.length === 0) {
|
||||||
if (!hasNextProfilesPage) {
|
|
||||||
// no items! remove the header
|
// no items! remove the header
|
||||||
i.pop()
|
i.pop()
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
i.push(...profileItems)
|
i.push(...profileItems)
|
||||||
}
|
}
|
||||||
if (hasNextProfilesPage) {
|
|
||||||
i.push({
|
|
||||||
type: 'loadMore',
|
|
||||||
key: 'loadMoreProfiles',
|
|
||||||
message: _(msg`Load more suggested accounts`),
|
|
||||||
isLoadingMore: isLoadingMoreProfiles,
|
|
||||||
onLoadMore: onLoadMoreProfiles,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
console.log('no pages')
|
// no items! remove the header
|
||||||
|
i.pop()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
i.push({type: 'profilePlaceholder', key: 'profilePlaceholder'})
|
i.push({type: 'profilePlaceholder', key: 'profilePlaceholder'})
|
||||||
@@ -414,14 +346,11 @@ export function Explore({
|
|||||||
}
|
}
|
||||||
return i
|
return i
|
||||||
}, [
|
}, [
|
||||||
profiles,
|
|
||||||
_,
|
_,
|
||||||
canShowSuggestedProfiles,
|
|
||||||
hasNextProfilesPage,
|
|
||||||
isLoadingMoreProfiles,
|
|
||||||
moderationOpts,
|
moderationOpts,
|
||||||
onLoadMoreProfiles,
|
suggestedUsers,
|
||||||
profilesError,
|
suggestedUsersIsLoading,
|
||||||
|
suggestedUsersError,
|
||||||
])
|
])
|
||||||
const suggestedFeedsModule = useMemo(() => {
|
const suggestedFeedsModule = useMemo(() => {
|
||||||
const i: ExploreScreenItems[] = []
|
const i: ExploreScreenItems[] = []
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ export function SuggestedAccountsTabBar({
|
|||||||
}}
|
}}
|
||||||
hasSearchText={false}
|
hasSearchText={false}
|
||||||
interestsDisplayNames={{
|
interestsDisplayNames={{
|
||||||
all: _(msg`All`),
|
all: _(msg`For You`),
|
||||||
...interestsDisplayNames,
|
...interestsDisplayNames,
|
||||||
}}
|
}}
|
||||||
TabComponent={Tab}
|
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 findAllProfilesInProfileFollowersQueryData} from '#/state/queries/profile-followers'
|
||||||
import {findAllProfilesInQueryData as findAllProfilesInProfileFollowsQueryData} from '#/state/queries/profile-follows'
|
import {findAllProfilesInQueryData as findAllProfilesInProfileFollowsQueryData} from '#/state/queries/profile-follows'
|
||||||
import {findAllProfilesInQueryData as findAllProfilesInSuggestedFollowsQueryData} from '#/state/queries/suggested-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 type * as bsky from '#/types/bsky'
|
||||||
import {castAsShadow, type Shadow} from './types'
|
import {castAsShadow, type Shadow} from './types'
|
||||||
|
|
||||||
@@ -149,6 +150,7 @@ function* findProfilesInCache(
|
|||||||
yield* findAllProfilesInProfileQueryData(queryClient, did)
|
yield* findAllProfilesInProfileQueryData(queryClient, did)
|
||||||
yield* findAllProfilesInProfileFollowersQueryData(queryClient, did)
|
yield* findAllProfilesInProfileFollowersQueryData(queryClient, did)
|
||||||
yield* findAllProfilesInProfileFollowsQueryData(queryClient, did)
|
yield* findAllProfilesInProfileFollowsQueryData(queryClient, did)
|
||||||
|
yield* findAllProfilesInSuggestedUsersQueryData(queryClient, did)
|
||||||
yield* findAllProfilesInSuggestedFollowsQueryData(queryClient, did)
|
yield* findAllProfilesInSuggestedFollowsQueryData(queryClient, did)
|
||||||
yield* findAllProfilesInActorSearchQueryData(queryClient, did)
|
yield* findAllProfilesInActorSearchQueryData(queryClient, did)
|
||||||
yield* findAllProfilesInListConvosQueryData(queryClient, did)
|
yield* findAllProfilesInListConvosQueryData(queryClient, did)
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user