Bluesky app fork with some witchin' additions 💫

[Explore] Reduced experience (#8160)

* Only show suggested users for non-english users

* Fall back to searching for users for non-english speakers

* Disable other queries if full experience is disabled

* Bump package

* If no content langs, use full exp

authored by

Eric Bailey and committed by
GitHub
c8568e30 09111ef2

+173 -62
+1 -1
package.json
··· 58 58 "icons:optimize": "svgo -f ./assets/icons" 59 59 }, 60 60 "dependencies": { 61 - "@atproto/api": "^0.14.20", 61 + "@atproto/api": "^0.14.21", 62 62 "@bitdrift/react-native": "^0.6.8", 63 63 "@braintree/sanitize-url": "^6.0.2", 64 64 "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
+76 -30
src/screens/Search/Explore.tsx
··· 8 8 import {msg, Trans} from '@lingui/macro' 9 9 import {useLingui} from '@lingui/react' 10 10 import {useQueryClient} from '@tanstack/react-query' 11 + import * as bcp47Match from 'bcp-47-match' 11 12 12 13 import {useGate} from '#/lib/statsig/statsig' 13 14 import {cleanError} from '#/lib/strings/errors' 14 15 import {sanitizeHandle} from '#/lib/strings/handles' 15 16 import {logger} from '#/logger' 16 17 import {type MetricEvents} from '#/logger/metrics' 18 + import {useLanguagePrefs} from '#/state/preferences/languages' 17 19 import {useModerationOpts} from '#/state/preferences/moderation-opts' 20 + import {RQKEY_ROOT_PAGINATED as useActorSearchPaginatedQueryKeyRoot} from '#/state/queries/actor-search' 18 21 import { 19 22 type FeedPreviewItem, 20 23 useFeedPreviews, ··· 26 29 createGetSuggestedFeedsQueryKey, 27 30 useGetSuggestedFeedsQuery, 28 31 } from '#/state/queries/trending/useGetSuggestedFeedsQuery' 29 - import { 30 - getSuggestedUsersQueryKeyRoot, 31 - useGetSuggestedUsersQuery, 32 - } from '#/state/queries/trending/useGetSuggestedUsersQuery' 32 + import {getSuggestedUsersQueryKeyRoot} from '#/state/queries/trending/useGetSuggestedUsersQuery' 33 33 import {createGetTrendsQueryKey} from '#/state/queries/trending/useGetTrendsQuery' 34 34 import { 35 35 createSuggestedStarterPacksQueryKey, ··· 43 43 import {FeedFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' 44 44 import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn' 45 45 import { 46 + popularInterests, 47 + useInterestsDisplayNames, 48 + } from '#/screens/Onboarding/state' 49 + import { 46 50 StarterPackCard, 47 51 StarterPackCardSkeleton, 48 52 } from '#/screens/Search/components/StarterPackCard' ··· 50 54 import {ExploreRecommendations} from '#/screens/Search/modules/ExploreRecommendations' 51 55 import {ExploreTrendingTopics} from '#/screens/Search/modules/ExploreTrendingTopics' 52 56 import {ExploreTrendingVideos} from '#/screens/Search/modules/ExploreTrendingVideos' 57 + import {useSuggestedUsers} from '#/screens/Search/util/useSuggestedUsers' 53 58 import {atoms as a, native, platform, useTheme} from '#/alf' 54 59 import {Admonition} from '#/components/Admonition' 55 60 import {Button} from '#/components/Button' ··· 64 69 import {UserCircle_Stroke2_Corner0_Rounded as Person} from '#/components/icons/UserCircle' 65 70 import {Loader} from '#/components/Loader' 66 71 import * as ProfileCard from '#/components/ProfileCard' 72 + import {boostInterests} from '#/components/ProgressGuide/FollowDialog' 67 73 import {SubtleHover} from '#/components/SubtleHover' 68 74 import {Text} from '#/components/Typography' 69 75 import * as ModuleHeader from './components/ModuleHeader' ··· 135 141 metricsTag: MetricEvents['explore:module:searchButtonPress']['module'] 136 142 tab: 'user' | 'profile' | 'feed' 137 143 } 144 + hideDefaultTab?: boolean 138 145 } 139 146 | { 140 147 type: 'trendingTopics' ··· 151 158 | { 152 159 type: 'profile' 153 160 key: string 154 - profile: AppBskyActorDefs.ProfileViewBasic 161 + profile: AppBskyActorDefs.ProfileView 155 162 recId?: number 156 163 } 157 164 | { ··· 212 219 const gate = useGate() 213 220 const guide = useProgressGuide('follow-10') 214 221 const [selectedInterest, setSelectedInterest] = useState<string | null>(null) 222 + 223 + /* 224 + * Begin special language handling 225 + */ 226 + const {contentLanguages} = useLanguagePrefs() 227 + const useFullExperience = useMemo(() => { 228 + if (contentLanguages.length === 0) return true 229 + return bcp47Match.basicFilter('en', contentLanguages).length > 0 230 + }, [contentLanguages]) 231 + const personalizedInterests = preferences?.interests?.tags 232 + const interestsDisplayNames = useInterestsDisplayNames() 233 + const interests = Object.keys(interestsDisplayNames) 234 + .sort(boostInterests(popularInterests)) 235 + .sort(boostInterests(personalizedInterests)) 215 236 const { 216 237 data: suggestedUsers, 217 238 isLoading: suggestedUsersIsLoading, 218 239 error: suggestedUsersError, 219 240 isRefetching: suggestedUsersIsRefetching, 220 - } = useGetSuggestedUsersQuery({ 221 - category: selectedInterest, 241 + } = useSuggestedUsers({ 242 + category: selectedInterest || (useFullExperience ? null : interests[0]), 243 + search: !useFullExperience, 222 244 }) 245 + /* End special language handling */ 246 + 223 247 const { 224 248 data: feeds, 225 249 hasNextPage: hasNextFeedsPage, ··· 227 251 isFetchingNextPage: isFetchingNextFeedsPage, 228 252 error: feedsError, 229 253 fetchNextPage: fetchNextFeedsPage, 230 - } = useGetPopularFeedsQuery({limit: 10}) 254 + } = useGetPopularFeedsQuery({limit: 10, enabled: useFullExperience}) 231 255 const interestsNux = useNux(Nux.ExploreInterestsCard) 232 256 const showInterestsNux = 233 257 interestsNux.status === 'ready' && !interestsNux.nux?.completed ··· 237 261 isLoading: isLoadingSuggestedSPs, 238 262 error: suggestedSPsError, 239 263 isRefetching: isRefetchingSuggestedSPs, 240 - } = useSuggestedStarterPacksQuery() 264 + } = useSuggestedStarterPacksQuery({enabled: useFullExperience}) 241 265 242 266 const isLoadingMoreFeeds = isFetchingNextFeedsPage && !isLoadingFeeds 243 267 const [hasPressedLoadMoreFeeds, setHasPressedLoadMoreFeeds] = useState(false) ··· 260 284 hasPressedLoadMoreFeeds, 261 285 ]) 262 286 263 - const {data: suggestedFeeds} = useGetSuggestedFeedsQuery() 287 + const {data: suggestedFeeds} = useGetSuggestedFeedsQuery({ 288 + enabled: useFullExperience, 289 + }) 264 290 const { 265 291 data: feedPreviewSlices, 266 292 query: { ··· 270 296 hasNextPage: hasNextPageFeedPreviews, 271 297 error: feedPreviewSlicesError, 272 298 }, 273 - } = useFeedPreviews(suggestedFeeds?.feeds ?? []) 299 + } = useFeedPreviews(suggestedFeeds?.feeds ?? [], useFullExperience) 274 300 275 301 const qc = useQueryClient() 276 302 const [isPTR, setIsPTR] = useState(false) ··· 285 311 }), 286 312 await qc.resetQueries({ 287 313 queryKey: [getSuggestedUsersQueryKeyRoot], 314 + }), 315 + await qc.resetQueries({ 316 + queryKey: [useActorSearchPaginatedQueryKeyRoot], 288 317 }), 289 318 await qc.resetQueries({ 290 319 queryKey: createGetSuggestedFeedsQueryKey(), ··· 334 363 metricsTag: 'suggestedAccounts', 335 364 tab: 'user', 336 365 }, 366 + hideDefaultTab: !useFullExperience, 337 367 }) 338 368 339 369 if (suggestedUsersIsLoading || suggestedUsersIsRefetching) { ··· 353 383 let seen = new Set() 354 384 const profileItems: ExploreScreenItems[] = [] 355 385 for (const actor of suggestedUsers.actors) { 386 + // checking for following still necessary if search data is used 356 387 if (!seen.has(actor.did) && !actor.viewer?.following) { 357 388 seen.add(actor.did) 358 389 profileItems.push({ ··· 369 400 key: 'profileEmpty', 370 401 }) 371 402 } else { 372 - if (selectedInterest === null) { 403 + if (selectedInterest === null && useFullExperience) { 373 404 // First "For You" tab, only show 5 to keep screen short 374 405 i.push(...profileItems.slice(0, 5)) 375 406 } else { ··· 395 426 suggestedUsersIsRefetching, 396 427 suggestedUsersError, 397 428 selectedInterest, 429 + useFullExperience, 398 430 ]) 399 431 const suggestedFeedsModule = useMemo(() => { 400 432 const i: ExploreScreenItems[] = [] ··· 565 597 566 598 i.push(topBorder) 567 599 i.push(...interestsNuxModule) 568 - if (isNewUser) { 569 - i.push(...suggestedFollowsModule) 570 - i.push(...suggestedStarterPacksModule) 571 - i.push({ 572 - type: 'header', 573 - key: 'trending-topics-header', 574 - title: _(msg`Trending topics`), 575 - icon: Graph, 576 - bottomBorder: true, 577 - }) 578 - i.push(trendingTopicsModule) 600 + 601 + if (useFullExperience) { 602 + if (isNewUser) { 603 + i.push(...suggestedFollowsModule) 604 + i.push(...suggestedStarterPacksModule) 605 + i.push({ 606 + type: 'header', 607 + key: 'trending-topics-header', 608 + title: _(msg`Trending topics`), 609 + icon: Graph, 610 + bottomBorder: true, 611 + }) 612 + i.push(trendingTopicsModule) 613 + } else { 614 + i.push(trendingTopicsModule) 615 + i.push(...suggestedFollowsModule) 616 + i.push(...suggestedStarterPacksModule) 617 + } 618 + if (gate('explore_show_suggested_feeds')) { 619 + i.push(...suggestedFeedsModule) 620 + } 621 + i.push(...feedPreviewsModule) 579 622 } else { 580 - i.push(trendingTopicsModule) 581 623 i.push(...suggestedFollowsModule) 582 - i.push(...suggestedStarterPacksModule) 583 624 } 584 - if (gate('explore_show_suggested_feeds')) { 585 - i.push(...suggestedFeedsModule) 586 - } 587 - i.push(...feedPreviewsModule) 588 625 589 626 return i 590 627 }, [ ··· 598 635 feedPreviewsModule, 599 636 interestsNuxModule, 600 637 gate, 638 + useFullExperience, 601 639 ]) 602 640 603 641 const renderItem = useCallback( ··· 641 679 <SuggestedAccountsTabBar 642 680 selectedInterest={selectedInterest} 643 681 onSelectInterest={setSelectedInterest} 682 + hideDefaultTab={item.hideDefaultTab} 644 683 /> 645 684 </View> 646 685 ) ··· 672 711 return ( 673 712 <View style={[a.px_lg, a.pb_lg]}> 674 713 <Admonition> 675 - <Trans>No results for "{selectedInterest}".</Trans> 714 + {selectedInterest ? ( 715 + <Trans> 716 + No results for "{interestsDisplayNames[selectedInterest]}". 717 + </Trans> 718 + ) : ( 719 + <Trans>No results.</Trans> 720 + )} 676 721 </Admonition> 677 722 </View> 678 723 ) ··· 876 921 focusSearchInput, 877 922 moderationOpts, 878 923 selectedInterest, 924 + interestsDisplayNames, 879 925 _, 880 926 fetchNextPageFeedPreviews, 881 927 ],
+14 -6
src/screens/Search/modules/ExploreSuggestedAccounts.tsx
··· 58 58 export function SuggestedAccountsTabBar({ 59 59 selectedInterest, 60 60 onSelectInterest, 61 + hideDefaultTab, 61 62 }: { 62 63 selectedInterest: string | null 63 64 onSelectInterest: (interest: string | null) => void 65 + hideDefaultTab?: boolean 64 66 }) { 65 67 const {_} = useLingui() 66 68 const interestsDisplayNames = useInterestsDisplayNames() ··· 72 74 return ( 73 75 <BlockDrawerGesture> 74 76 <Tabs 75 - interests={['all', ...interests]} 76 - selectedInterest={selectedInterest || 'all'} 77 + interests={hideDefaultTab ? interests : ['all', ...interests]} 78 + selectedInterest={ 79 + selectedInterest || (hideDefaultTab ? interests[0] : 'all') 80 + } 77 81 onSelectTab={tab => { 78 82 logger.metric( 79 83 'explore:suggestedAccounts:tabPressed', ··· 83 87 onSelectInterest(tab === 'all' ? null : tab) 84 88 }} 85 89 hasSearchText={false} 86 - interestsDisplayNames={{ 87 - all: _(msg`For You`), 88 - ...interestsDisplayNames, 89 - }} 90 + interestsDisplayNames={ 91 + hideDefaultTab 92 + ? interestsDisplayNames 93 + : { 94 + all: _(msg`For You`), 95 + ...interestsDisplayNames, 96 + } 97 + } 90 98 TabComponent={Tab} 91 99 contentContainerStyle={[ 92 100 {
+56
src/screens/Search/util/useSuggestedUsers.ts
··· 1 + import {useMemo} from 'react' 2 + 3 + import {useActorSearchPaginated} from '#/state/queries/actor-search' 4 + import {useGetSuggestedUsersQuery} from '#/state/queries/trending/useGetSuggestedUsersQuery' 5 + import {useInterestsDisplayNames} from '#/screens/Onboarding/state' 6 + 7 + /** 8 + * Conditional hook, used in case a user is a non-english speaker, in which 9 + * case we fall back to searching for users instead of our more curated set. 10 + */ 11 + export function useSuggestedUsers({ 12 + category = null, 13 + search = false, 14 + }: { 15 + category?: string | null 16 + /** 17 + * If true, we'll search for users using the translated value of `category`, 18 + * based on the user's "app language setting 19 + */ 20 + search?: boolean 21 + }) { 22 + const interestsDisplayNames = useInterestsDisplayNames() 23 + const curated = useGetSuggestedUsersQuery({ 24 + enabled: !search, 25 + category, 26 + }) 27 + const searched = useActorSearchPaginated({ 28 + enabled: !!search, 29 + // use user's app language translation for this value 30 + query: category ? interestsDisplayNames[category] : '', 31 + limit: 10, 32 + }) 33 + 34 + return useMemo(() => { 35 + if (search) { 36 + return { 37 + // we're not paginating right now 38 + data: searched?.data 39 + ? { 40 + actors: searched.data.pages.flatMap(p => p.actors) ?? [], 41 + } 42 + : undefined, 43 + isLoading: searched.isLoading, 44 + error: searched.error, 45 + isRefetching: searched.isRefetching, 46 + } 47 + } else { 48 + return { 49 + data: curated.data, 50 + isLoading: curated.isLoading, 51 + error: curated.error, 52 + isRefetching: curated.isRefetching, 53 + } 54 + } 55 + }, [curated, searched, search]) 56 + }
+1 -1
src/state/queries/actor-search.ts
··· 17 17 const RQKEY_ROOT = 'actor-search' 18 18 export const RQKEY = (query: string) => [RQKEY_ROOT, query] 19 19 20 - const RQKEY_ROOT_PAGINATED = `${RQKEY_ROOT}_paginated` 20 + export const RQKEY_ROOT_PAGINATED = `${RQKEY_ROOT}_paginated` 21 21 export const RQKEY_PAGINATED = (query: string, limit?: number) => [ 22 22 RQKEY_ROOT_PAGINATED, 23 23 query,
+2 -1
src/state/queries/explore-feed-previews.tsx
··· 120 120 121 121 export function useFeedPreviews( 122 122 feedsMaybeWithDuplicates: AppBskyFeedDefs.GeneratorView[], 123 + isEnabled: boolean = true, 123 124 ) { 124 125 const feeds = useMemo( 125 126 () => ··· 135 136 const {data: preferences} = usePreferencesQuery() 136 137 const userInterests = aggregateUserInterests(preferences) 137 138 const moderationOpts = useModerationOpts() 138 - const enabled = feeds.length > 0 139 + const enabled = feeds.length > 0 && isEnabled 139 140 140 141 const query = useInfiniteQuery({ 141 142 enabled,
+11 -11
src/state/queries/feed.ts
··· 1 1 import {useCallback, useEffect, useMemo, useRef} from 'react' 2 2 import { 3 - AppBskyActorDefs, 4 - AppBskyFeedDefs, 5 - AppBskyGraphDefs, 6 - AppBskyUnspeccedGetPopularFeedGenerators, 3 + type AppBskyActorDefs, 4 + type AppBskyFeedDefs, 5 + type AppBskyGraphDefs, 6 + type AppBskyUnspeccedGetPopularFeedGenerators, 7 7 AtUri, 8 8 moderateFeedGenerator, 9 9 RichText, 10 10 } from '@atproto/api' 11 11 import { 12 - InfiniteData, 12 + type InfiniteData, 13 13 keepPreviousData, 14 - QueryClient, 15 - QueryKey, 14 + type QueryClient, 15 + type QueryKey, 16 16 useInfiniteQuery, 17 17 useMutation, 18 18 useQuery, ··· 28 28 import {useAgent, useSession} from '#/state/session' 29 29 import {router} from '#/routes' 30 30 import {useModerationOpts} from '../preferences/moderation-opts' 31 - import {FeedDescriptor} from './post-feed' 31 + import {type FeedDescriptor} from './post-feed' 32 32 import {precacheResolvedUri} from './resolve-uri' 33 33 34 34 export type FeedSourceFeedInfo = { ··· 203 203 'at://did:plc:vpkhqolt662uhesyj6nxm7ys/app.bsky.feed.generator/followpics', // the gram, by why 204 204 ] 205 205 206 - type GetPopularFeedsOptions = {limit?: number} 206 + type GetPopularFeedsOptions = {limit?: number; enabled?: boolean} 207 207 208 208 export function createGetPopularFeedsQueryKey( 209 209 options?: GetPopularFeedsOptions, 210 210 ) { 211 - return ['getPopularFeeds', options] 211 + return ['getPopularFeeds', options?.limit] 212 212 } 213 213 214 214 export function useGetPopularFeedsQuery(options?: GetPopularFeedsOptions) { ··· 237 237 QueryKey, 238 238 string | undefined 239 239 >({ 240 - enabled: Boolean(moderationOpts), 240 + enabled: Boolean(moderationOpts) && options?.enabled !== false, 241 241 queryKey: createGetPopularFeedsQueryKey(options), 242 242 queryFn: async ({pageParam}) => { 243 243 const res = await agent.app.bsky.unspecced.getPopularFeedGenerators({
+2 -2
src/state/queries/trending/useGetSuggestedFeedsQuery.ts
··· 13 13 14 14 export const createGetSuggestedFeedsQueryKey = () => ['suggested-feeds'] 15 15 16 - export function useGetSuggestedFeedsQuery() { 16 + export function useGetSuggestedFeedsQuery({enabled}: {enabled?: boolean}) { 17 17 const agent = useAgent() 18 18 const {data: preferences} = usePreferencesQuery() 19 19 const savedFeeds = preferences?.savedFeeds 20 20 21 21 return useQuery({ 22 - enabled: !!preferences, 22 + enabled: !!preferences && enabled !== false, 23 23 staleTime: STALE.MINUTES.THREE, 24 24 queryKey: createGetSuggestedFeedsQueryKey(), 25 25 queryFn: async () => {
+4 -4
src/state/queries/trending/useGetSuggestedUsersQuery.ts
··· 13 13 import {usePreferencesQuery} from '#/state/queries/preferences' 14 14 import {useAgent} from '#/state/session' 15 15 16 - export type QueryProps = {category?: string | null} 16 + export type QueryProps = {category?: string | null; enabled?: boolean} 17 17 18 18 export const getSuggestedUsersQueryKeyRoot = 'unspecced-suggested-users' 19 19 export const createGetSuggestedUsersQueryKey = (props: QueryProps) => [ 20 20 getSuggestedUsersQueryKeyRoot, 21 - ...Object.values(props), 21 + props.category, 22 22 ] 23 23 24 24 export function useGetSuggestedUsersQuery(props: QueryProps) { ··· 26 26 const {data: preferences} = usePreferencesQuery() 27 27 28 28 return useQuery({ 29 - enabled: !!preferences, 29 + enabled: !!preferences && props.enabled, 30 30 staleTime: STALE.MINUTES.THREE, 31 31 queryKey: createGetSuggestedUsersQueryKey(props), 32 32 queryFn: async () => { ··· 52 52 export function* findAllProfilesInQueryData( 53 53 queryClient: QueryClient, 54 54 did: string, 55 - ): Generator<AppBskyActorDefs.ProfileViewBasic, void> { 55 + ): Generator<AppBskyActorDefs.ProfileView, void> { 56 56 const responses = 57 57 queryClient.getQueriesData<AppBskyUnspeccedGetSuggestedUsers.OutputSchema>({ 58 58 queryKey: [getSuggestedUsersQueryKeyRoot],
+2 -2
src/state/queries/useSuggestedStarterPacksQuery.ts
··· 13 13 'suggested-starter-packs', 14 14 ] 15 15 16 - export function useSuggestedStarterPacksQuery() { 16 + export function useSuggestedStarterPacksQuery({enabled}: {enabled?: boolean}) { 17 17 const agent = useAgent() 18 18 const {data: preferences} = usePreferencesQuery() 19 19 const contentLangs = getContentLanguages().join(',') 20 20 21 21 return useQuery({ 22 - enabled: !!preferences, 22 + enabled: !!preferences && enabled !== false, 23 23 staleTime: STALE.MINUTES.THREE, 24 24 queryKey: createSuggestedStarterPacksQueryKey(), 25 25 async queryFn() {
+4 -4
yarn.lock
··· 80 80 tlds "^1.234.0" 81 81 zod "^3.23.8" 82 82 83 - "@atproto/api@^0.14.20": 84 - version "0.14.20" 85 - resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.14.20.tgz#904c85a91748f3203fd929415cb8fb3bc78d35d3" 86 - integrity sha512-Daip22+u9N+EVPk9PsEEVrTfjIqGczXnAT7o2EHGd0JsOzMbp3a6wmW1beKqYDzPf+Dc36/39JeUYYqhB3fKjg== 83 + "@atproto/api@^0.14.21": 84 + version "0.14.21" 85 + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.14.21.tgz#29c189b7dba316945cf7317b9ded49b1b60d3ad9" 86 + integrity sha512-hCIcjks/snscH3ZtZFoicQN2hRM5MpWQUvvzyIa265XQ2vSv5BP+gsQVIHWtYKt+gzwq1E7jY4us6c4N7fsLlQ== 87 87 dependencies: 88 88 "@atproto/common-web" "^0.4.1" 89 89 "@atproto/lexicon" "^0.4.10"