Compare commits

...

5 Commits

Author SHA1 Message Date
Eric Bailey 3498794777 If no content langs, use full exp 2025-04-08 14:40:17 -05:00
Eric Bailey 306a513a31 Bump package 2025-04-08 14:38:30 -05:00
Eric Bailey 41298228a1 Disable other queries if full experience is disabled 2025-04-08 14:29:18 -05:00
Eric Bailey 052ffcf1d9 Fall back to searching for users for non-english speakers 2025-04-08 14:24:04 -05:00
Eric Bailey f3d979b316 Only show suggested users for non-english users 2025-04-08 13:39:46 -05:00
11 changed files with 173 additions and 62 deletions
+1 -1
View File
@@ -58,7 +58,7 @@
"icons:optimize": "svgo -f ./assets/icons"
},
"dependencies": {
"@atproto/api": "^0.14.20",
"@atproto/api": "^0.14.21",
"@bitdrift/react-native": "^0.6.8",
"@braintree/sanitize-url": "^6.0.2",
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
+76 -30
View File
@@ -8,6 +8,7 @@ import {
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
import * as bcp47Match from 'bcp-47-match'
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
import {useGate} from '#/lib/statsig/statsig'
@@ -15,7 +16,9 @@ import {cleanError} from '#/lib/strings/errors'
import {sanitizeHandle} from '#/lib/strings/handles'
import {logger} from '#/logger'
import {type MetricEvents} from '#/logger/metrics'
import {useLanguagePrefs} from '#/state/preferences/languages'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {RQKEY_ROOT_PAGINATED as useActorSearchPaginatedQueryKeyRoot} from '#/state/queries/actor-search'
import {
type FeedPreviewItem,
useFeedPreviews,
@@ -27,10 +30,7 @@ import {
createGetSuggestedFeedsQueryKey,
useGetSuggestedFeedsQuery,
} from '#/state/queries/trending/useGetSuggestedFeedsQuery'
import {
getSuggestedUsersQueryKeyRoot,
useGetSuggestedUsersQuery,
} from '#/state/queries/trending/useGetSuggestedUsersQuery'
import {getSuggestedUsersQueryKeyRoot} from '#/state/queries/trending/useGetSuggestedUsersQuery'
import {createGetTrendsQueryKey} from '#/state/queries/trending/useGetTrendsQuery'
import {
createSuggestedStarterPacksQueryKey,
@@ -43,6 +43,10 @@ import {ViewFullThread} from '#/view/com/posts/ViewFullThread'
import {List} from '#/view/com/util/List'
import {FeedFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn'
import {
popularInterests,
useInterestsDisplayNames,
} from '#/screens/Onboarding/state'
import {
StarterPackCard,
StarterPackCardSkeleton,
@@ -51,6 +55,7 @@ import {ExploreInterestsCard} from '#/screens/Search/modules/ExploreInterestsCar
import {ExploreRecommendations} from '#/screens/Search/modules/ExploreRecommendations'
import {ExploreTrendingTopics} from '#/screens/Search/modules/ExploreTrendingTopics'
import {ExploreTrendingVideos} from '#/screens/Search/modules/ExploreTrendingVideos'
import {useSuggestedUsers} from '#/screens/Search/util/useSuggestedUsers'
import {atoms as a, native, platform, useTheme} from '#/alf'
import {Admonition} from '#/components/Admonition'
import {Button} from '#/components/Button'
@@ -65,6 +70,7 @@ import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Tre
import {UserCircle_Stroke2_Corner0_Rounded as Person} from '#/components/icons/UserCircle'
import {Loader} from '#/components/Loader'
import * as ProfileCard from '#/components/ProfileCard'
import {boostInterests} from '#/components/ProgressGuide/FollowDialog'
import {SubtleHover} from '#/components/SubtleHover'
import {Text} from '#/components/Typography'
import * as ModuleHeader from './components/ModuleHeader'
@@ -136,6 +142,7 @@ type ExploreScreenItems =
metricsTag: MetricEvents['explore:module:searchButtonPress']['module']
tab: 'user' | 'profile' | 'feed'
}
hideDefaultTab?: boolean
}
| {
type: 'trendingTopics'
@@ -152,7 +159,7 @@ type ExploreScreenItems =
| {
type: 'profile'
key: string
profile: AppBskyActorDefs.ProfileViewBasic
profile: AppBskyActorDefs.ProfileView
recId?: number
}
| {
@@ -214,14 +221,31 @@ export function Explore({
const gate = useGate()
const guide = useProgressGuide('follow-10')
const [selectedInterest, setSelectedInterest] = useState<string | null>(null)
/*
* Begin special language handling
*/
const {contentLanguages} = useLanguagePrefs()
const useFullExperience = useMemo(() => {
if (contentLanguages.length === 0) return true
return bcp47Match.basicFilter('en', contentLanguages).length > 0
}, [contentLanguages])
const personalizedInterests = preferences?.interests?.tags
const interestsDisplayNames = useInterestsDisplayNames()
const interests = Object.keys(interestsDisplayNames)
.sort(boostInterests(popularInterests))
.sort(boostInterests(personalizedInterests))
const {
data: suggestedUsers,
isLoading: suggestedUsersIsLoading,
error: suggestedUsersError,
isRefetching: suggestedUsersIsRefetching,
} = useGetSuggestedUsersQuery({
category: selectedInterest,
} = useSuggestedUsers({
category: selectedInterest || (useFullExperience ? null : interests[0]),
search: !useFullExperience,
})
/* End special language handling */
const {
data: feeds,
hasNextPage: hasNextFeedsPage,
@@ -229,7 +253,7 @@ export function Explore({
isFetchingNextPage: isFetchingNextFeedsPage,
error: feedsError,
fetchNextPage: fetchNextFeedsPage,
} = useGetPopularFeedsQuery({limit: 10})
} = useGetPopularFeedsQuery({limit: 10, enabled: useFullExperience})
const interestsNux = useNux(Nux.ExploreInterestsCard)
const showInterestsNux =
interestsNux.status === 'ready' && !interestsNux.nux?.completed
@@ -239,7 +263,7 @@ export function Explore({
isLoading: isLoadingSuggestedSPs,
error: suggestedSPsError,
isRefetching: isRefetchingSuggestedSPs,
} = useSuggestedStarterPacksQuery()
} = useSuggestedStarterPacksQuery({enabled: useFullExperience})
const isLoadingMoreFeeds = isFetchingNextFeedsPage && !isLoadingFeeds
const [hasPressedLoadMoreFeeds, setHasPressedLoadMoreFeeds] = useState(false)
@@ -262,7 +286,9 @@ export function Explore({
hasPressedLoadMoreFeeds,
])
const {data: suggestedFeeds} = useGetSuggestedFeedsQuery()
const {data: suggestedFeeds} = useGetSuggestedFeedsQuery({
enabled: useFullExperience,
})
const {
data: feedPreviewSlices,
query: {
@@ -272,7 +298,7 @@ export function Explore({
hasNextPage: hasNextPageFeedPreviews,
error: feedPreviewSlicesError,
},
} = useFeedPreviews(suggestedFeeds?.feeds ?? [])
} = useFeedPreviews(suggestedFeeds?.feeds ?? [], useFullExperience)
const qc = useQueryClient()
const [isPTR, setIsPTR] = useState(false)
@@ -288,6 +314,9 @@ export function Explore({
await qc.resetQueries({
queryKey: [getSuggestedUsersQueryKeyRoot],
}),
await qc.resetQueries({
queryKey: [useActorSearchPaginatedQueryKeyRoot],
}),
await qc.resetQueries({
queryKey: createGetSuggestedFeedsQueryKey(),
}),
@@ -336,6 +365,7 @@ export function Explore({
metricsTag: 'suggestedAccounts',
tab: 'user',
},
hideDefaultTab: !useFullExperience,
})
if (suggestedUsersIsLoading || suggestedUsersIsRefetching) {
@@ -355,6 +385,7 @@ export function Explore({
let seen = new Set()
const profileItems: ExploreScreenItems[] = []
for (const actor of suggestedUsers.actors) {
// checking for following still necessary if search data is used
if (!seen.has(actor.did) && !actor.viewer?.following) {
seen.add(actor.did)
profileItems.push({
@@ -371,7 +402,7 @@ export function Explore({
key: 'profileEmpty',
})
} else {
if (selectedInterest === null) {
if (selectedInterest === null && useFullExperience) {
// First "For You" tab, only show 5 to keep screen short
i.push(...profileItems.slice(0, 5))
} else {
@@ -397,6 +428,7 @@ export function Explore({
suggestedUsersIsRefetching,
suggestedUsersError,
selectedInterest,
useFullExperience,
])
const suggestedFeedsModule = useMemo(() => {
const i: ExploreScreenItems[] = []
@@ -567,26 +599,31 @@ export function Explore({
i.push(topBorder)
i.push(...interestsNuxModule)
if (isNewUser) {
i.push(...suggestedFollowsModule)
i.push(...suggestedStarterPacksModule)
i.push({
type: 'header',
key: 'trending-topics-header',
title: _(msg`Trending topics`),
icon: Graph,
bottomBorder: true,
})
i.push(trendingTopicsModule)
if (useFullExperience) {
if (isNewUser) {
i.push(...suggestedFollowsModule)
i.push(...suggestedStarterPacksModule)
i.push({
type: 'header',
key: 'trending-topics-header',
title: _(msg`Trending topics`),
icon: Graph,
bottomBorder: true,
})
i.push(trendingTopicsModule)
} else {
i.push(trendingTopicsModule)
i.push(...suggestedFollowsModule)
i.push(...suggestedStarterPacksModule)
}
if (gate('explore_show_suggested_feeds')) {
i.push(...suggestedFeedsModule)
}
i.push(...feedPreviewsModule)
} else {
i.push(trendingTopicsModule)
i.push(...suggestedFollowsModule)
i.push(...suggestedStarterPacksModule)
}
if (gate('explore_show_suggested_feeds')) {
i.push(...suggestedFeedsModule)
}
i.push(...feedPreviewsModule)
return i
}, [
@@ -600,6 +637,7 @@ export function Explore({
feedPreviewsModule,
interestsNuxModule,
gate,
useFullExperience,
])
const renderItem = useCallback(
@@ -643,6 +681,7 @@ export function Explore({
<SuggestedAccountsTabBar
selectedInterest={selectedInterest}
onSelectInterest={setSelectedInterest}
hideDefaultTab={item.hideDefaultTab}
/>
</View>
)
@@ -674,7 +713,13 @@ export function Explore({
return (
<View style={[a.px_lg, a.pb_lg]}>
<Admonition>
<Trans>No results for "{selectedInterest}".</Trans>
{selectedInterest ? (
<Trans>
No results for "{interestsDisplayNames[selectedInterest]}".
</Trans>
) : (
<Trans>No results.</Trans>
)}
</Admonition>
</View>
)
@@ -878,6 +923,7 @@ export function Explore({
focusSearchInput,
moderationOpts,
selectedInterest,
interestsDisplayNames,
_,
fetchNextPageFeedPreviews,
],
@@ -58,9 +58,11 @@ export function useLoadEnoughProfiles({
export function SuggestedAccountsTabBar({
selectedInterest,
onSelectInterest,
hideDefaultTab,
}: {
selectedInterest: string | null
onSelectInterest: (interest: string | null) => void
hideDefaultTab?: boolean
}) {
const {_} = useLingui()
const interestsDisplayNames = useInterestsDisplayNames()
@@ -72,8 +74,10 @@ export function SuggestedAccountsTabBar({
return (
<BlockDrawerGesture>
<Tabs
interests={['all', ...interests]}
selectedInterest={selectedInterest || 'all'}
interests={hideDefaultTab ? interests : ['all', ...interests]}
selectedInterest={
selectedInterest || (hideDefaultTab ? interests[0] : 'all')
}
onSelectTab={tab => {
logger.metric(
'explore:suggestedAccounts:tabPressed',
@@ -83,10 +87,14 @@ export function SuggestedAccountsTabBar({
onSelectInterest(tab === 'all' ? null : tab)
}}
hasSearchText={false}
interestsDisplayNames={{
all: _(msg`For You`),
...interestsDisplayNames,
}}
interestsDisplayNames={
hideDefaultTab
? interestsDisplayNames
: {
all: _(msg`For You`),
...interestsDisplayNames,
}
}
TabComponent={Tab}
contentContainerStyle={[
{
@@ -0,0 +1,56 @@
import {useMemo} from 'react'
import {useActorSearchPaginated} from '#/state/queries/actor-search'
import {useGetSuggestedUsersQuery} from '#/state/queries/trending/useGetSuggestedUsersQuery'
import {useInterestsDisplayNames} from '#/screens/Onboarding/state'
/**
* Conditional hook, used in case a user is a non-english speaker, in which
* case we fall back to searching for users instead of our more curated set.
*/
export function useSuggestedUsers({
category = null,
search = false,
}: {
category?: string | null
/**
* If true, we'll search for users using the translated value of `category`,
* based on the user's "app language setting
*/
search?: boolean
}) {
const interestsDisplayNames = useInterestsDisplayNames()
const curated = useGetSuggestedUsersQuery({
enabled: !search,
category,
})
const searched = useActorSearchPaginated({
enabled: !!search,
// use user's app language translation for this value
query: category ? interestsDisplayNames[category] : '',
limit: 10,
})
return useMemo(() => {
if (search) {
return {
// we're not paginating right now
data: searched?.data
? {
actors: searched.data.pages.flatMap(p => p.actors) ?? [],
}
: undefined,
isLoading: searched.isLoading,
error: searched.error,
isRefetching: searched.isRefetching,
}
} else {
return {
data: curated.data,
isLoading: curated.isLoading,
error: curated.error,
isRefetching: curated.isRefetching,
}
}
}, [curated, searched, search])
}
+1 -1
View File
@@ -17,7 +17,7 @@ import {useAgent} from '#/state/session'
const RQKEY_ROOT = 'actor-search'
export const RQKEY = (query: string) => [RQKEY_ROOT, query]
const RQKEY_ROOT_PAGINATED = `${RQKEY_ROOT}_paginated`
export const RQKEY_ROOT_PAGINATED = `${RQKEY_ROOT}_paginated`
export const RQKEY_PAGINATED = (query: string, limit?: number) => [
RQKEY_ROOT_PAGINATED,
query,
+2 -1
View File
@@ -120,6 +120,7 @@ export type FeedPreviewItem =
export function useFeedPreviews(
feedsMaybeWithDuplicates: AppBskyFeedDefs.GeneratorView[],
isEnabled: boolean = true,
) {
const feeds = useMemo(
() =>
@@ -135,7 +136,7 @@ export function useFeedPreviews(
const {data: preferences} = usePreferencesQuery()
const userInterests = aggregateUserInterests(preferences)
const moderationOpts = useModerationOpts()
const enabled = feeds.length > 0
const enabled = feeds.length > 0 && isEnabled
const query = useInfiniteQuery({
enabled,
+11 -11
View File
@@ -1,18 +1,18 @@
import {useCallback, useEffect, useMemo, useRef} from 'react'
import {
AppBskyActorDefs,
AppBskyFeedDefs,
AppBskyGraphDefs,
AppBskyUnspeccedGetPopularFeedGenerators,
type AppBskyActorDefs,
type AppBskyFeedDefs,
type AppBskyGraphDefs,
type AppBskyUnspeccedGetPopularFeedGenerators,
AtUri,
moderateFeedGenerator,
RichText,
} from '@atproto/api'
import {
InfiniteData,
type InfiniteData,
keepPreviousData,
QueryClient,
QueryKey,
type QueryClient,
type QueryKey,
useInfiniteQuery,
useMutation,
useQuery,
@@ -28,7 +28,7 @@ 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 {type FeedDescriptor} from './post-feed'
import {precacheResolvedUri} from './resolve-uri'
export type FeedSourceFeedInfo = {
@@ -203,12 +203,12 @@ export const KNOWN_AUTHED_ONLY_FEEDS = [
'at://did:plc:vpkhqolt662uhesyj6nxm7ys/app.bsky.feed.generator/followpics', // the gram, by why
]
type GetPopularFeedsOptions = {limit?: number}
type GetPopularFeedsOptions = {limit?: number; enabled?: boolean}
export function createGetPopularFeedsQueryKey(
options?: GetPopularFeedsOptions,
) {
return ['getPopularFeeds', options]
return ['getPopularFeeds', options?.limit]
}
export function useGetPopularFeedsQuery(options?: GetPopularFeedsOptions) {
@@ -237,7 +237,7 @@ export function useGetPopularFeedsQuery(options?: GetPopularFeedsOptions) {
QueryKey,
string | undefined
>({
enabled: Boolean(moderationOpts),
enabled: Boolean(moderationOpts) && options?.enabled !== false,
queryKey: createGetPopularFeedsQueryKey(options),
queryFn: async ({pageParam}) => {
const res = await agent.app.bsky.unspecced.getPopularFeedGenerators({
@@ -13,13 +13,13 @@ export const DEFAULT_LIMIT = 5
export const createGetSuggestedFeedsQueryKey = () => ['suggested-feeds']
export function useGetSuggestedFeedsQuery() {
export function useGetSuggestedFeedsQuery({enabled}: {enabled?: boolean}) {
const agent = useAgent()
const {data: preferences} = usePreferencesQuery()
const savedFeeds = preferences?.savedFeeds
return useQuery({
enabled: !!preferences,
enabled: !!preferences && enabled !== false,
staleTime: STALE.MINUTES.THREE,
queryKey: createGetSuggestedFeedsQueryKey(),
queryFn: async () => {
@@ -13,12 +13,12 @@ import {STALE} from '#/state/queries'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {useAgent} from '#/state/session'
export type QueryProps = {category?: string | null}
export type QueryProps = {category?: string | null; enabled?: boolean}
export const getSuggestedUsersQueryKeyRoot = 'unspecced-suggested-users'
export const createGetSuggestedUsersQueryKey = (props: QueryProps) => [
getSuggestedUsersQueryKeyRoot,
...Object.values(props),
props.category,
]
export function useGetSuggestedUsersQuery(props: QueryProps) {
@@ -26,7 +26,7 @@ export function useGetSuggestedUsersQuery(props: QueryProps) {
const {data: preferences} = usePreferencesQuery()
return useQuery({
enabled: !!preferences,
enabled: !!preferences && props.enabled,
staleTime: STALE.MINUTES.THREE,
queryKey: createGetSuggestedUsersQueryKey(props),
queryFn: async () => {
@@ -52,7 +52,7 @@ export function useGetSuggestedUsersQuery(props: QueryProps) {
export function* findAllProfilesInQueryData(
queryClient: QueryClient,
did: string,
): Generator<AppBskyActorDefs.ProfileViewBasic, void> {
): Generator<AppBskyActorDefs.ProfileView, void> {
const responses =
queryClient.getQueriesData<AppBskyUnspeccedGetSuggestedUsers.OutputSchema>({
queryKey: [getSuggestedUsersQueryKeyRoot],
@@ -13,13 +13,13 @@ export const createSuggestedStarterPacksQueryKey = () => [
'suggested-starter-packs',
]
export function useSuggestedStarterPacksQuery() {
export function useSuggestedStarterPacksQuery({enabled}: {enabled?: boolean}) {
const agent = useAgent()
const {data: preferences} = usePreferencesQuery()
const contentLangs = getContentLanguages().join(',')
return useQuery({
enabled: !!preferences,
enabled: !!preferences && enabled !== false,
staleTime: STALE.MINUTES.THREE,
queryKey: createSuggestedStarterPacksQueryKey(),
async queryFn() {
+4 -4
View File
@@ -80,10 +80,10 @@
tlds "^1.234.0"
zod "^3.23.8"
"@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==
"@atproto/api@^0.14.21":
version "0.14.21"
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.14.21.tgz#29c189b7dba316945cf7317b9ded49b1b60d3ad9"
integrity sha512-hCIcjks/snscH3ZtZFoicQN2hRM5MpWQUvvzyIa265XQ2vSv5BP+gsQVIHWtYKt+gzwq1E7jY4us6c4N7fsLlQ==
dependencies:
"@atproto/common-web" "^0.4.1"
"@atproto/lexicon" "^0.4.10"