Merge remote-tracking branch 'origin/main' into explore/refetch

This commit is contained in:
Hailey
2025-04-04 17:01:00 -07:00
15 changed files with 334 additions and 155 deletions
+1 -1
View File
@@ -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",
+3 -3
View File
@@ -19,7 +19,7 @@ export function AvatarStack({
numPending?: number
backgroundColor?: string
}) {
const halfSize = size / 2
const translation = size / 3 // overlap by 1/3
const t = useTheme()
const moderationOpts = useModerationOpts()
@@ -43,7 +43,7 @@ export function AvatarStack({
a.flex_row,
a.align_center,
a.relative,
{width: size + (items.length - 1) * halfSize},
{width: size + (items.length - 1) * (size - translation)},
]}>
{items.map((item, i) => (
<View
@@ -54,7 +54,7 @@ export function AvatarStack({
{
width: size,
height: size,
left: i * -halfSize,
left: i * -translation,
borderWidth: 1,
borderColor: backgroundColor ?? t.atoms.bg.backgroundColor,
borderRadius: 999,
+4 -4
View File
@@ -152,7 +152,7 @@ export function AvatarPlaceholder() {
<View
style={[
a.rounded_full,
t.atoms.bg_contrast_50,
t.atoms.bg_contrast_25,
{
width: 40,
height: 40,
@@ -203,7 +203,7 @@ export function NameAndHandlePlaceholder() {
<View
style={[
a.rounded_xs,
t.atoms.bg_contrast_50,
t.atoms.bg_contrast_25,
{
width: '60%',
height: 14,
@@ -214,7 +214,7 @@ export function NameAndHandlePlaceholder() {
<View
style={[
a.rounded_xs,
t.atoms.bg_contrast_50,
t.atoms.bg_contrast_25,
{
width: '40%',
height: 10,
@@ -275,7 +275,7 @@ export function DescriptionPlaceholder({
style={[
a.rounded_xs,
a.w_full,
t.atoms.bg_contrast_50,
t.atoms.bg_contrast_25,
{height: 12, width: i + 1 === numberOfLines ? '60%' : '100%'},
]}
/>
+11 -2
View File
@@ -1,5 +1,12 @@
import {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'
import {ScrollView, TextInput, useWindowDimensions, View} from 'react-native'
import {
ScrollView,
type StyleProp,
TextInput,
useWindowDimensions,
View,
type ViewStyle,
} from 'react-native'
import Animated, {
LayoutAnimationConfig,
LinearTransition,
@@ -453,6 +460,7 @@ let Tabs = ({
hasSearchText,
interestsDisplayNames,
TabComponent = Tab,
contentContainerStyle,
}: {
onSelectTab: (tab: string) => void
interests: string[]
@@ -460,6 +468,7 @@ let Tabs = ({
hasSearchText: boolean
interestsDisplayNames: Record<string, string>
TabComponent?: React.ComponentType<React.ComponentProps<typeof Tab>>
contentContainerStyle?: StyleProp<ViewStyle>
}): React.ReactNode => {
const listRef = useRef<ScrollView>(null)
const [scrollX, setScrollX] = useState(0)
@@ -520,7 +529,7 @@ let Tabs = ({
<ScrollView
ref={listRef}
horizontal
contentContainerStyle={[a.gap_sm, a.px_lg]}
contentContainerStyle={[a.gap_sm, a.px_lg, contentContainerStyle]}
showsHorizontalScrollIndicator={false}
decelerationRate="fast"
snapToOffsets={
+57 -101
View File
@@ -15,15 +15,15 @@ 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,
} from '#/state/queries/explore-feed-previews'
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'
@@ -36,6 +36,7 @@ import {
StarterPackCard,
StarterPackCardSkeleton,
} from '#/screens/Search/components/StarterPackCard'
import {ExploreInterestsCard} from '#/screens/Search/modules/ExploreInterestsCard'
import {ExploreRecommendations} from '#/screens/Search/modules/ExploreRecommendations'
import {ExploreTrendingTopics} from '#/screens/Search/modules/ExploreTrendingTopics'
import {ExploreTrendingVideos} from '#/screens/Search/modules/ExploreTrendingVideos'
@@ -44,6 +45,7 @@ import {Button} from '#/components/Button'
import * as FeedCard from '#/components/FeedCard'
import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDownIcon} from '#/components/icons/Chevron'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {type Props as IcoProps} from '#/components/icons/common'
import {type Props as SVGIconProps} from '#/components/icons/common'
import {ListSparkle_Stroke2_Corner0_Rounded as ListSparkle} from '#/components/icons/ListSparkle'
import {StarterPack} from '#/components/icons/StarterPack'
@@ -55,7 +57,6 @@ import * as ModuleHeader from './components/ModuleHeader'
import {
SuggestedAccountsTabBar,
SuggestedProfileCard,
useLoadEnoughProfiles,
} from './modules/ExploreSuggestedAccounts'
function LoadMore({item}: {item: ExploreScreenItems & {type: 'loadMore'}}) {
@@ -110,6 +111,7 @@ type ExploreScreenItems =
key: string
title: string
icon: React.ComponentType<SVGIconProps>
iconSize?: IcoProps['size']
searchButton?: {
label: string
metricsTag: MetricEvents['explore:module:searchButtonPress']['module']
@@ -142,7 +144,7 @@ type ExploreScreenItems =
| {
type: 'profile'
key: string
profile: AppBskyActorDefs.ProfileView
profile: AppBskyActorDefs.ProfileViewBasic
recId?: number
}
| {
@@ -181,6 +183,10 @@ type ExploreScreenItems =
key: string
}
| FeedPreviewItem
| {
type: 'interests-card'
key: 'interests-card'
}
export function Explore({
focusSearchInput,
@@ -197,33 +203,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,
@@ -233,40 +219,10 @@ export function Explore({
error: feedsError,
fetchNextPage: fetchNextFeedsPage,
} = useGetPopularFeedsQuery({limit: 10})
const interestsNux = useNux(Nux.ExploreInterestsCard)
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,
@@ -349,55 +305,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'})
@@ -405,14 +348,11 @@ export function Explore({
}
return i
}, [
profiles,
_,
canShowSuggestedProfiles,
hasNextProfilesPage,
isLoadingMoreProfiles,
moderationOpts,
onLoadMoreProfiles,
profilesError,
suggestedUsers,
suggestedUsersIsLoading,
suggestedUsersError,
])
const suggestedFeedsModule = useMemo(() => {
const i: ExploreScreenItems[] = []
@@ -523,6 +463,7 @@ export function Explore({
key: 'suggested-starterPacks-header',
title: _(msg`Starter Packs`),
icon: StarterPack,
iconSize: 'xl',
})
if (isLoadingSuggestedSPs) {
@@ -558,6 +499,16 @@ export function Explore({
return i
}, [feedPreviewSlices, isFetchingNextPageFeedPreviews])
const interestsNuxModule = useMemo<ExploreScreenItems[]>(() => {
if (!showInterestsNux) return []
return [
{
type: 'interests-card',
key: 'interests-card',
},
]
}, [showInterestsNux])
const isNewUser = guide?.guide === 'follow-10' && !guide.isComplete
const items = useMemo<ExploreScreenItems[]>(() => {
const i: ExploreScreenItems[] = []
@@ -565,6 +516,7 @@ export function Explore({
// Dynamic module ordering
i.push(topBorder)
i.push(...interestsNuxModule)
if (isNewUser) {
i.push(...suggestedFollowsModule)
i.push(...suggestedStarterPacksModule)
@@ -588,6 +540,7 @@ export function Explore({
suggestedFeedsModule,
trendingTopicsModule,
feedPreviewsModule,
interestsNuxModule,
gate,
])
@@ -612,7 +565,7 @@ export function Explore({
case 'header': {
return (
<ModuleHeader.Container>
<ModuleHeader.Icon icon={item.icon} />
<ModuleHeader.Icon icon={item.icon} size={item.iconSize} />
<ModuleHeader.TitleText>{item.title}</ModuleHeader.TitleText>
{item.searchButton && (
<ModuleHeader.SearchButton
@@ -854,6 +807,9 @@ export function Explore({
/>
)
}
case 'interests-card': {
return <ExploreInterestsCard />
}
}
},
[
+1 -1
View File
@@ -318,7 +318,7 @@ export function SearchScreenShell({
</Layout.Header.Outer>
</View>
)}
<View style={[a.px_md, a.pt_sm, a.pb_sm, a.overflow_hidden]}>
<View style={[a.px_lg, a.pt_sm, a.pb_sm, a.overflow_hidden]}>
<View style={[a.gap_sm]}>
<View style={[a.w_full, a.flex_row, a.align_stretch, a.gap_xs]}>
<View style={[a.flex_1]}>
+2 -10
View File
@@ -6,14 +6,7 @@ import {PressableScale} from '#/lib/custom-animations/PressableScale'
import {makeCustomFeedLink} from '#/lib/routes/links'
import {logger} from '#/logger'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {
atoms as a,
native,
useGutters,
useTheme,
type ViewStyleProp,
web,
} from '#/alf'
import {atoms as a, native, useTheme, type ViewStyleProp, web} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button'
import * as FeedCard from '#/components/FeedCard'
import {sizes as iconSizes} from '#/components/icons/common'
@@ -27,13 +20,12 @@ export function Container({
headerHeight,
}: {children: React.ReactNode; headerHeight?: number} & ViewStyleProp) {
const t = useTheme()
const gutters = useGutters([0, 'base'])
return (
<View
style={[
gutters,
a.flex_row,
a.align_center,
a.px_lg,
a.pt_2xl,
a.pb_md,
a.gap_sm,
@@ -234,7 +234,7 @@ export function AvatarStack({
{computedTotal > 0 ? (
<Text
style={[
gtPhone ? a.text_md : a.text_sm,
gtPhone ? a.text_md : a.text_xs,
a.font_bold,
a.leading_snug,
{color: 'white'},
@@ -0,0 +1,127 @@
import {useState} from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {Nux, useSaveNux} from '#/state/queries/nuxs'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {useInterestsDisplayNames} from '#/screens/Onboarding/state'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
import {Link} from '#/components/Link'
import * as Prompt from '#/components/Prompt'
import {Text} from '#/components/Typography'
export function ExploreInterestsCard() {
const t = useTheme()
const {_} = useLingui()
const {data: preferences} = usePreferencesQuery()
const interestsDisplayNames = useInterestsDisplayNames()
const {mutateAsync: saveNux} = useSaveNux()
const trendingPrompt = Prompt.usePromptControl()
const [closing, setClosing] = useState(false)
const onClose = () => {
trendingPrompt.open()
}
const onConfirmClose = () => {
setClosing(true)
// if this fails, they can try again later
saveNux({
id: Nux.ExploreInterestsCard,
completed: true,
data: undefined,
}).catch(() => {})
}
return closing ? null : (
<>
<Prompt.Basic
control={trendingPrompt}
title={_(msg`Your interests`)}
description={_(
msg`You can adjust your interests at any time from your "Content and media" settings.`,
)}
confirmButtonCta={_(
msg({
message: `Copy that!`,
comment: `Confirm button text. Can be a short cheeky phrase that means "OK" e.g. "Copy that!"`,
}),
)}
onConfirm={onConfirmClose}
/>
<View style={[a.p_lg, a.pb_2xs]}>
<View
style={[
a.p_lg,
a.rounded_md,
a.border,
a.gap_sm,
t.atoms.border_contrast_medium,
t.atoms.bg_contrast_25,
]}>
<Text style={[a.text_md, a.font_bold, a.leading_tight]}>
<Trans>Your interests</Trans>
</Text>
{preferences?.interests?.tags &&
preferences.interests.tags.length > 0 ? (
<View style={[a.flex_row, a.flex_wrap, {gap: 6}]}>
{preferences.interests.tags.map(tag => (
<View
key={tag}
style={[
a.justify_center,
a.align_center,
a.rounded_full,
a.border,
t.atoms.border_contrast_medium,
a.px_lg,
{height: 32},
]}>
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
{interestsDisplayNames[tag]}
</Text>
</View>
))}
</View>
) : null}
<Text style={[a.text_sm, a.leading_snug, a.pb_xs]}>
<Trans>
Your selected interests help us serve you content you care about.
</Trans>
</Text>
<Link
label={_(msg`Edit interests`)}
to="/settings/interests"
size="small"
variant="solid"
color="primary"
style={[a.justify_center]}>
<ButtonText>
<Trans>Edit interests</Trans>
</ButtonText>
</Link>
<Button
label={_(msg`Hide this card`)}
size="small"
variant="solid"
color="secondary"
shape="round"
onPress={onClose}
style={[
a.absolute,
{top: a.pt_xs.paddingTop, right: a.pr_xs.paddingRight},
]}>
<ButtonIcon icon={X} />
</Button>
</View>
</View>
</>
)
}
@@ -83,10 +83,16 @@ export function SuggestedAccountsTabBar({
}}
hasSearchText={false}
interestsDisplayNames={{
all: _(msg`All`),
all: _(msg`For You`),
...interestsDisplayNames,
}}
TabComponent={Tab}
contentContainerStyle={[
{
// visual alignment
paddingLeft: a.px_md.paddingLeft,
},
]}
/>
</BlockDrawerGesture>
)
+29 -21
View File
@@ -16,6 +16,7 @@ import {useAgent} from '#/state/session'
import * as Toast from '#/view/com/util/Toast'
import {useInterestsDisplayNames} from '#/screens/Onboarding/state'
import {atoms as a, useGutters, useTheme} from '#/alf'
import {Admonition} from '#/components/Admonition'
import {Divider} from '#/components/Divider'
import * as Toggle from '#/components/forms/Toggle'
import * as Layout from '#/components/Layout'
@@ -49,8 +50,7 @@ export function SettingsInterests() {
t.atoms.text_contrast_medium,
]}>
<Trans>
Selecting interests from the list below helps us deliver you
higher quality content.
Your selected interests help us serve you content you care about.
</Trans>
</Text>
@@ -135,25 +135,33 @@ function Inner({
}
return (
<Toggle.Group
values={interests}
onChange={onChangeInterests}
label={_(msg`Select your interests from the options below`)}>
<View style={[a.flex_row, a.flex_wrap, a.gap_sm]}>
{INTERESTS.map(interest => {
const name = interestsDisplayNames[interest]
if (!name) return null
return (
<Toggle.Item
key={interest}
name={interest}
label={interestsDisplayNames[interest]}>
<InterestButton interest={interest} />
</Toggle.Item>
)
})}
</View>
</Toggle.Group>
<>
{interests.length === 0 && (
<Admonition type="tip">
<Trans>We recommend selecting at least two interests.</Trans>
</Admonition>
)}
<Toggle.Group
values={interests}
onChange={onChangeInterests}
label={_(msg`Select your interests from the options below`)}>
<View style={[a.flex_row, a.flex_wrap, a.gap_sm]}>
{INTERESTS.map(interest => {
const name = interestsDisplayNames[interest]
if (!name) return null
return (
<Toggle.Item
key={interest}
name={interest}
label={interestsDisplayNames[interest]}>
<InterestButton interest={interest} />
</Toggle.Item>
)
})}
</View>
</Toggle.Group>
</>
)
}
+2
View File
@@ -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)
+14 -6
View File
@@ -1,18 +1,26 @@
import zod from 'zod'
import type zod from 'zod'
import {BaseNux} from '#/state/queries/nuxs/types'
import {type BaseNux} from '#/state/queries/nuxs/types'
export enum Nux {
NeueTypography = 'NeueTypography',
ExploreInterestsCard = 'ExploreInterestsCard',
}
export const nuxNames = new Set(Object.values(Nux))
export type AppNux = BaseNux<{
id: Nux.NeueTypography
data: undefined
}>
export type AppNux = BaseNux<
| {
id: Nux.NeueTypography
data: undefined
}
| {
id: Nux.ExploreInterestsCard
data: undefined
}
>
export const NuxSchemas: Record<Nux, zod.ZodObject<any> | undefined> = {
[Nux.NeueTypography]: undefined,
[Nux.ExploreInterestsCard]: undefined,
}
@@ -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
}
}
}
}
+4 -4
View File
@@ -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"