Bluesky app fork with some witchin' additions 💫

[Explore] New suggested follows endpoint (#8130)

* Bump SDK

* Integrate new endpoint, add profile shadow, For You tab

* Format

authored by

Eric Bailey and committed by
GitHub
aca89d4a a0ff9b52

+108 -106
+1 -1
package.json
··· 58 58 "icons:optimize": "svgo -f ./assets/icons" 59 59 }, 60 60 "dependencies": { 61 - "@atproto/api": "^0.14.19", 61 + "@atproto/api": "^0.14.20", 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",
+29 -100
src/screens/Search/Explore.tsx
··· 15 15 import {logger} from '#/logger' 16 16 import {type MetricEvents} from '#/logger/metrics' 17 17 import {useModerationOpts} from '#/state/preferences/moderation-opts' 18 - import {useActorSearchPaginated} from '#/state/queries/actor-search' 19 18 import { 20 19 type FeedPreviewItem, 21 20 useFeedPreviews, ··· 23 22 import {useGetPopularFeedsQuery} from '#/state/queries/feed' 24 23 import {Nux, useNux} from '#/state/queries/nuxs' 25 24 import {usePreferencesQuery} from '#/state/queries/preferences' 26 - import {useSuggestedFollowsQuery} from '#/state/queries/suggested-follows' 27 25 import {useGetSuggestedFeedsQuery} from '#/state/queries/trending/useGetSuggestedFeedsQuery' 26 + import {useGetSuggestedUsersQuery} from '#/state/queries/trending/useGetSuggestedUsersQuery' 28 27 import {useSuggestedStarterPacksQuery} from '#/state/queries/useSuggestedStarterPacksQuery' 29 28 import {useProgressGuide} from '#/state/shell/progress-guide' 30 29 import {isThreadChildAt, isThreadParentAt} from '#/view/com/posts/PostFeed' ··· 57 56 import { 58 57 SuggestedAccountsTabBar, 59 58 SuggestedProfileCard, 60 - useLoadEnoughProfiles, 61 59 } from './modules/ExploreSuggestedAccounts' 62 60 63 61 function LoadMore({item}: {item: ExploreScreenItems & {type: 'loadMore'}}) { ··· 144 142 | { 145 143 type: 'profile' 146 144 key: string 147 - profile: AppBskyActorDefs.ProfileView 145 + profile: AppBskyActorDefs.ProfileViewBasic 148 146 recId?: number 149 147 } 150 148 | { ··· 203 201 const gate = useGate() 204 202 const guide = useProgressGuide('follow-10') 205 203 const [selectedInterest, setSelectedInterest] = useState<string | null>(null) 204 + // TODO always get at least 10 back 206 205 const { 207 - data: suggestedProfiles, 208 - hasNextPage: hasNextSuggestedProfilesPage, 209 - isLoading: isLoadingSuggestedProfiles, 210 - isFetchingNextPage: isFetchingNextSuggestedProfilesPage, 211 - error: suggestedProfilesError, 212 - fetchNextPage: fetchNextSuggestedProfilesPage, 213 - } = useSuggestedFollowsQuery({limit: 3, subsequentPageLimit: 10}) 214 - const { 215 - data: interestProfiles, 216 - hasNextPage: hasNextInterestProfilesPage, 217 - isLoading: isLoadingInterestProfiles, 218 - isFetchingNextPage: isFetchingNextInterestProfilesPage, 219 - error: interestProfilesError, 220 - fetchNextPage: fetchNextInterestProfilesPage, 221 - } = useActorSearchPaginated({ 222 - query: selectedInterest || '', 223 - enabled: !!selectedInterest, 224 - limit: 10, 225 - }) 226 - const {isReady: canShowSuggestedProfiles} = useLoadEnoughProfiles({ 227 - interest: selectedInterest, 228 - data: interestProfiles, 229 - isLoading: isLoadingInterestProfiles, 230 - isFetchingNextPage: isFetchingNextInterestProfilesPage, 231 - hasNextPage: hasNextInterestProfilesPage, 232 - fetchNextPage: fetchNextInterestProfilesPage, 206 + data: suggestedUsers, 207 + isLoading: suggestedUsersIsLoading, 208 + error: suggestedUsersError, 209 + } = useGetSuggestedUsersQuery({ 210 + category: selectedInterest, 233 211 }) 234 212 const { 235 213 data: feeds, ··· 243 221 const showInterestsNux = 244 222 interestsNux.status === 'ready' && !interestsNux.nux?.completed 245 223 246 - const profiles: typeof suggestedProfiles & typeof interestProfiles = 247 - !selectedInterest ? suggestedProfiles : interestProfiles 248 - const hasNextProfilesPage = !selectedInterest 249 - ? hasNextSuggestedProfilesPage 250 - : hasNextInterestProfilesPage 251 - const isLoadingProfiles = !selectedInterest 252 - ? isLoadingSuggestedProfiles 253 - : !canShowSuggestedProfiles 254 - const isFetchingNextProfilesPage = !selectedInterest 255 - ? isFetchingNextSuggestedProfilesPage 256 - : !canShowSuggestedProfiles 257 - const profilesError = !selectedInterest 258 - ? suggestedProfilesError 259 - : interestProfilesError 260 - const fetchNextProfilesPage = !selectedInterest 261 - ? fetchNextSuggestedProfilesPage 262 - : fetchNextInterestProfilesPage 263 - 264 - const isLoadingMoreProfiles = isFetchingNextProfilesPage && !isLoadingProfiles 265 - const onLoadMoreProfiles = useCallback(async () => { 266 - if (isFetchingNextProfilesPage || !hasNextProfilesPage || profilesError) 267 - return 268 - try { 269 - await fetchNextProfilesPage() 270 - } catch (err) { 271 - logger.error('Failed to load more suggested follows', {message: err}) 272 - } 273 - }, [ 274 - isFetchingNextProfilesPage, 275 - hasNextProfilesPage, 276 - profilesError, 277 - fetchNextProfilesPage, 278 - ]) 279 224 const { 280 225 data: suggestedSPs, 281 226 isLoading: isLoadingSuggestedSPs, ··· 358 303 }, 359 304 }) 360 305 361 - if (!canShowSuggestedProfiles) { 306 + if (suggestedUsersIsLoading) { 362 307 i.push({type: 'profilePlaceholder', key: 'profilePlaceholder'}) 363 - } else if (profilesError) { 308 + } else if (suggestedUsersError) { 364 309 i.push({ 365 310 type: 'error', 366 - key: 'profilesError', 311 + key: 'suggestedUsersError', 367 312 message: _(msg`Failed to load suggested follows`), 368 - error: cleanError(profilesError), 313 + error: cleanError(suggestedUsersError), 369 314 }) 370 315 } else { 371 - if (profiles !== undefined) { 372 - if (profiles.pages.length > 0 && moderationOpts) { 316 + if (suggestedUsers !== undefined) { 317 + if (suggestedUsers.actors.length > 0 && moderationOpts) { 373 318 // Currently the responses contain duplicate items. 374 319 // Needs to be fixed on backend, but let's dedupe to be safe. 375 320 let seen = new Set() 376 321 const profileItems: ExploreScreenItems[] = [] 377 - for (const page of profiles.pages) { 378 - for (const actor of page.actors) { 379 - if (!seen.has(actor.did) && !actor.viewer?.following) { 380 - seen.add(actor.did) 381 - profileItems.push({ 382 - type: 'profile', 383 - key: actor.did, 384 - profile: actor, 385 - recId: page.recId, 386 - }) 387 - } 322 + for (const actor of suggestedUsers.actors) { 323 + if (!seen.has(actor.did) && !actor.viewer?.following) { 324 + seen.add(actor.did) 325 + profileItems.push({ 326 + type: 'profile', 327 + key: actor.did, 328 + profile: actor, 329 + }) 388 330 } 389 331 } 390 332 391 333 if (profileItems.length === 0) { 392 - if (!hasNextProfilesPage) { 393 - // no items! remove the header 394 - i.pop() 395 - } 334 + // no items! remove the header 335 + i.pop() 396 336 } else { 397 337 i.push(...profileItems) 398 338 } 399 - if (hasNextProfilesPage) { 400 - i.push({ 401 - type: 'loadMore', 402 - key: 'loadMoreProfiles', 403 - message: _(msg`Load more suggested accounts`), 404 - isLoadingMore: isLoadingMoreProfiles, 405 - onLoadMore: onLoadMoreProfiles, 406 - }) 407 - } 408 339 } else { 409 - console.log('no pages') 340 + // no items! remove the header 341 + i.pop() 410 342 } 411 343 } else { 412 344 i.push({type: 'profilePlaceholder', key: 'profilePlaceholder'}) ··· 414 346 } 415 347 return i 416 348 }, [ 417 - profiles, 418 349 _, 419 - canShowSuggestedProfiles, 420 - hasNextProfilesPage, 421 - isLoadingMoreProfiles, 422 350 moderationOpts, 423 - onLoadMoreProfiles, 424 - profilesError, 351 + suggestedUsers, 352 + suggestedUsersIsLoading, 353 + suggestedUsersError, 425 354 ]) 426 355 const suggestedFeedsModule = useMemo(() => { 427 356 const i: ExploreScreenItems[] = []
+1 -1
src/screens/Search/modules/ExploreSuggestedAccounts.tsx
··· 83 83 }} 84 84 hasSearchText={false} 85 85 interestsDisplayNames={{ 86 - all: _(msg`All`), 86 + all: _(msg`For You`), 87 87 ...interestsDisplayNames, 88 88 }} 89 89 TabComponent={Tab}
+2
src/state/cache/profile-shadow.ts
··· 19 19 import {findAllProfilesInQueryData as findAllProfilesInProfileFollowersQueryData} from '#/state/queries/profile-followers' 20 20 import {findAllProfilesInQueryData as findAllProfilesInProfileFollowsQueryData} from '#/state/queries/profile-follows' 21 21 import {findAllProfilesInQueryData as findAllProfilesInSuggestedFollowsQueryData} from '#/state/queries/suggested-follows' 22 + import {findAllProfilesInQueryData as findAllProfilesInSuggestedUsersQueryData} from '#/state/queries/trending/useGetSuggestedUsersQuery' 22 23 import type * as bsky from '#/types/bsky' 23 24 import {castAsShadow, type Shadow} from './types' 24 25 ··· 149 150 yield* findAllProfilesInProfileQueryData(queryClient, did) 150 151 yield* findAllProfilesInProfileFollowersQueryData(queryClient, did) 151 152 yield* findAllProfilesInProfileFollowsQueryData(queryClient, did) 153 + yield* findAllProfilesInSuggestedUsersQueryData(queryClient, did) 152 154 yield* findAllProfilesInSuggestedFollowsQueryData(queryClient, did) 153 155 yield* findAllProfilesInActorSearchQueryData(queryClient, did) 154 156 yield* findAllProfilesInListConvosQueryData(queryClient, did)
+71
src/state/queries/trending/useGetSuggestedUsersQuery.ts
··· 1 + import { 2 + type AppBskyActorDefs, 3 + type AppBskyUnspeccedGetSuggestedUsers, 4 + } from '@atproto/api' 5 + import {type QueryClient, useQuery} from '@tanstack/react-query' 6 + 7 + import { 8 + aggregateUserInterests, 9 + createBskyTopicsHeader, 10 + } from '#/lib/api/feed/utils' 11 + import {getContentLanguages} from '#/state/preferences/languages' 12 + import {STALE} from '#/state/queries' 13 + import {usePreferencesQuery} from '#/state/queries/preferences' 14 + import {useAgent} from '#/state/session' 15 + 16 + export type QueryProps = {category?: string | null} 17 + 18 + export const getSuggestedUsersQueryKeyRoot = 'unspecced-suggested-users' 19 + export const createGetSuggestedUsersQueryKey = (props: QueryProps) => [ 20 + getSuggestedUsersQueryKeyRoot, 21 + ...Object.values(props), 22 + ] 23 + 24 + export function useGetSuggestedUsersQuery(props: QueryProps) { 25 + const agent = useAgent() 26 + const {data: preferences} = usePreferencesQuery() 27 + 28 + return useQuery({ 29 + enabled: !!preferences, 30 + refetchOnWindowFocus: true, 31 + staleTime: STALE.MINUTES.ONE, 32 + queryKey: createGetSuggestedUsersQueryKey(props), 33 + queryFn: async () => { 34 + const contentLangs = getContentLanguages().join(',') 35 + const {data} = await agent.app.bsky.unspecced.getSuggestedUsers( 36 + { 37 + category: props.category ?? undefined, 38 + }, 39 + { 40 + headers: { 41 + ...createBskyTopicsHeader(aggregateUserInterests(preferences)), 42 + 'Accept-Language': contentLangs, 43 + }, 44 + }, 45 + ) 46 + 47 + return data 48 + }, 49 + }) 50 + } 51 + 52 + export function* findAllProfilesInQueryData( 53 + queryClient: QueryClient, 54 + did: string, 55 + ): Generator<AppBskyActorDefs.ProfileViewBasic, void> { 56 + const responses = 57 + queryClient.getQueriesData<AppBskyUnspeccedGetSuggestedUsers.OutputSchema>({ 58 + queryKey: [getSuggestedUsersQueryKeyRoot], 59 + }) 60 + for (const [_, response] of responses) { 61 + if (!response) { 62 + continue 63 + } 64 + 65 + for (const actor of response.actors) { 66 + if (actor.did === did) { 67 + yield actor 68 + } 69 + } 70 + } 71 + }
+4 -4
yarn.lock
··· 80 80 tlds "^1.234.0" 81 81 zod "^3.23.8" 82 82 83 - "@atproto/api@^0.14.19": 84 - version "0.14.19" 85 - resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.14.19.tgz#fef8994e2b14e69a9e3a0aef043c7fcb34d6bf8c" 86 - integrity sha512-YYTqM0K0qk2TP7PguktPzlAQGLTL1bEGz6PgY5kqKJNX4o1318kJYB22DzjJYqV2NUCq0JQ9Lb0oskLvTisEOg== 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== 87 87 dependencies: 88 88 "@atproto/common-web" "^0.4.1" 89 89 "@atproto/lexicon" "^0.4.10"