Mirror — see github.com/blacksky-algorithms/blacksky.community
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

Remove broken onboarding steps and fetch profile data from Bluesky

Remove suggested-accounts, suggested-starterpacks, and find-contacts
onboarding steps since their backing API endpoints return empty or
error on our appview. Onboarding flow is now profile -> interests ->
finished.

Remove the backfill-in-progress badge from profile headers. Instead,
fetch follower/following/post counts and known followers from Bluesky's
appview via atproto-proxy, falling back to local data only when a user
is suspended on Bluesky but not on our side.

+80 -294
+1 -1
src/screens/Onboarding/StepFindContactsIntro/index.tsx
··· 81 81 </ButtonText> 82 82 </Button> 83 83 <Button 84 - onPress={() => dispatch({type: 'skip-contacts'})} 84 + onPress={() => dispatch({type: 'next'})} 85 85 label={_(msg`Skip`)} 86 86 size="large" 87 87 color="secondary">
+4 -92
src/screens/Onboarding/StepFinished/index.tsx
··· 3 3 import { 4 4 type AppBskyActorDefs, 5 5 type AppBskyActorProfile, 6 - type AppBskyGraphDefs, 7 - AppBskyGraphStarterpack, 8 6 type Un$Typed, 9 7 } from '@atproto/api' 10 8 import {TID} from '@atproto/common-web' ··· 22 20 } from '#/lib/constants' 23 21 import {useRequestNotificationsPermission} from '#/lib/notifications/notifications' 24 22 import {logger} from '#/logger' 25 - import {useSetHasCheckedForStarterPack} from '#/state/preferences/used-starter-packs' 26 - import {getAllListMembers} from '#/state/queries/list-members' 27 23 import {preferencesQueryKey} from '#/state/queries/preferences' 28 24 import {RQKEY as profileRQKey} from '#/state/queries/profile' 29 25 import {useAgent} from '#/state/session' 30 26 import {useOnboardingDispatch} from '#/state/shell' 31 27 import {useProgressGuideControls} from '#/state/shell/progress-guide' 32 28 import { 33 - useActiveStarterPack, 34 - useSetActiveStarterPack, 35 - } from '#/state/shell/starter-pack' 36 - import { 37 29 OnboardingControls, 38 30 OnboardingHeaderSlot, 39 31 } from '#/screens/Onboarding/Layout' ··· 48 40 import {Loader} from '#/components/Loader' 49 41 import {useAnalytics} from '#/analytics' 50 42 import {IS_WEB} from '#/env' 51 - import * as bsky from '#/types/bsky' 52 43 import {ValuePropositionPager} from './ValuePropositionPager' 53 44 54 45 export function StepFinished() { ··· 59 50 const queryClient = useQueryClient() 60 51 const agent = useAgent() 61 52 const requestNotificationsPermission = useRequestNotificationsPermission() 62 - const activeStarterPack = useActiveStarterPack() 63 - const setActiveStarterPack = useSetActiveStarterPack() 64 - const setHasCheckedForStarterPack = useSetHasCheckedForStarterPack() 65 53 const {startProgressGuide} = useProgressGuideControls() 66 54 67 55 const finishOnboarding = useCallback(async () => { 68 56 setSaving(true) 69 57 70 - let starterPack: AppBskyGraphDefs.StarterPackView | undefined 71 - let listItems: AppBskyGraphDefs.ListItemView[] | undefined 72 - 73 - if (activeStarterPack?.uri) { 74 - try { 75 - const spRes = await agent.app.bsky.graph.getStarterPack({ 76 - starterPack: activeStarterPack.uri, 77 - }) 78 - starterPack = spRes.data.starterPack 79 - } catch (e) { 80 - logger.error('Failed to fetch starter pack', {safeMessage: e}) 81 - // don't tell the user, just get them through onboarding. 82 - } 83 - try { 84 - if (starterPack?.list) { 85 - listItems = await getAllListMembers(agent, starterPack.list.uri) 86 - } 87 - } catch (e) { 88 - logger.error('Failed to fetch starter pack list items', { 89 - safeMessage: e, 90 - }) 91 - // don't tell the user, just get them through onboarding. 92 - } 93 - } 94 - 95 58 try { 96 59 const {interestsStepResults, profileStepResults} = state 97 60 const {selectedInterests} = interestsStepResults 98 61 99 62 await Promise.all([ 100 - bulkWriteFollows( 101 - agent, 102 - [ 103 - BSKY_APP_ACCOUNT_DID, 104 - BLACKSKY_COMMUNITY_DID, 105 - ...(listItems?.map(i => i.subject.did) ?? []), 106 - ], 107 - starterPack 108 - ? {uri: starterPack.uri, cid: starterPack.cid} 109 - : undefined, 110 - ), 63 + bulkWriteFollows(agent, [BSKY_APP_ACCOUNT_DID, BLACKSKY_COMMUNITY_DID]), 111 64 (async () => { 112 65 // Interests need to get saved first, then we can write the feeds to prefs 113 66 await agent.setInterestsPref({tags: selectedInterests}) ··· 128 81 }, 129 82 ] 130 83 131 - // Any starter pack feeds will be pinned _after_ the defaults 132 - if (starterPack && starterPack.feeds?.length) { 133 - feedsToSave.push( 134 - ...starterPack.feeds.map(f => ({ 135 - type: 'feed', 136 - value: f.uri, 137 - pinned: true, 138 - id: TID.nextStr(), 139 - })), 140 - ) 141 - } 142 - 143 84 await agent.overwriteSavedFeeds(feedsToSave) 144 85 })(), 145 86 (async () => { ··· 156 97 const res = await blobPromise 157 98 if (res.data.blob) { 158 99 next.avatar = res.data.blob 159 - } 160 - } 161 - 162 - if (starterPack) { 163 - next.joinedViaStarterPack = { 164 - uri: starterPack.uri, 165 - cid: starterPack.cid, 166 100 } 167 101 } 168 102 ··· 204 138 }) 205 139 206 140 setSaving(false) 207 - setActiveStarterPack(undefined) 208 - setHasCheckedForStarterPack(true) 209 141 startProgressGuide('follow-10') 210 142 dispatch({type: 'finish'}) 211 143 onboardDispatch({type: 'finish'}) 212 144 ax.metric('onboarding:finished:nextPressed', { 213 - usedStarterPack: Boolean(starterPack), 214 - starterPackName: 215 - starterPack && 216 - bsky.dangerousIsType<AppBskyGraphStarterpack.Record>( 217 - starterPack.record, 218 - AppBskyGraphStarterpack.isRecord, 219 - ) 220 - ? starterPack.record.name 221 - : undefined, 222 - starterPackCreator: starterPack?.creator.did, 223 - starterPackUri: starterPack?.uri, 224 - profilesFollowed: listItems?.length ?? 0, 225 - feedsPinned: starterPack?.feeds?.length ?? 0, 145 + usedStarterPack: false, 146 + profilesFollowed: 0, 147 + feedsPinned: 0, 226 148 }) 227 - if (starterPack && listItems?.length) { 228 - ax.metric('starterPack:followAll', { 229 - logContext: 'Onboarding', 230 - starterPack: starterPack.uri, 231 - count: listItems?.length, 232 - }) 233 - } 234 149 }, [ 235 150 ax, 236 151 queryClient, 237 152 agent, 238 153 dispatch, 239 154 onboardDispatch, 240 - activeStarterPack, 241 155 state, 242 156 requestNotificationsPermission, 243 - setActiveStarterPack, 244 - setHasCheckedForStarterPack, 245 157 startProgressGuide, 246 158 ]) 247 159
+6 -55
src/screens/Onboarding/index.tsx
··· 1 1 import {useMemo, useReducer} from 'react' 2 2 import {View} from 'react-native' 3 - import * as bcp47Match from 'bcp-47-match' 4 3 5 - import {useLanguagePrefs} from '#/state/preferences' 6 4 import { 7 5 Layout, 8 6 OnboardingControls, ··· 17 15 import {StepInterests} from '#/screens/Onboarding/StepInterests' 18 16 import {StepProfile} from '#/screens/Onboarding/StepProfile' 19 17 import {atoms as a, useTheme} from '#/alf' 20 - import {useIsFindContactsFeatureEnabledBasedOnGeolocation} from '#/components/contacts/country-allowlist' 21 - import {useFindContactsFlowState} from '#/components/contacts/state' 22 18 import {Portal} from '#/components/Portal' 23 19 import {ScreenTransition} from '#/components/ScreenTransition' 24 - import {useAnalytics} from '#/analytics' 25 - import {ENV, IS_NATIVE} from '#/env' 26 - import {StepFindContacts} from './StepFindContacts' 27 - import {StepFindContactsIntro} from './StepFindContactsIntro' 28 - import {StepSuggestedAccounts} from './StepSuggestedAccounts' 29 - import {StepSuggestedStarterpacks} from './StepSuggestedStarterpacks' 30 20 31 21 export function Onboarding() { 32 22 const t = useTheme() 33 - const ax = useAnalytics() 34 - 35 - const {contentLanguages} = useLanguagePrefs() 36 - const probablySpeaksEnglish = useMemo(() => { 37 - if (contentLanguages.length === 0) return true 38 - return bcp47Match.basicFilter('en', contentLanguages).length > 0 39 - }, [contentLanguages]) 40 - 41 - // starter packs screen is currently geared towards english-speaking accounts 42 - const showSuggestedStarterpacks = ENV !== 'e2e' && probablySpeaksEnglish 43 - 44 - const findContactsEnabled = 45 - useIsFindContactsFeatureEnabledBasedOnGeolocation() 46 - const showFindContacts = 47 - ENV !== 'e2e' && 48 - IS_NATIVE && 49 - findContactsEnabled && 50 - !ax.features.enabled(ax.features.ImportContactsOnboardingDisable) 51 23 52 24 const [state, dispatch] = useReducer( 53 25 reducer, 54 - { 55 - starterPacksStepEnabled: showSuggestedStarterpacks, 56 - findContactsStepEnabled: showFindContacts, 57 - }, 26 + undefined, 58 27 createInitialOnboardingState, 59 28 ) 60 - const [contactsFlowState, contactsFlowDispatch] = useFindContactsFlowState() 61 29 62 30 return ( 63 31 <Portal> ··· 70 38 key={state.activeStep} 71 39 direction={state.stepTransitionDirection} 72 40 style={a.flex_1}> 73 - {/* FindContactsFlow cannot be nested in Layout */} 74 - {state.activeStep === 'find-contacts' ? ( 75 - <StepFindContacts 76 - flowState={contactsFlowState} 77 - flowDispatch={contactsFlowDispatch} 78 - /> 79 - ) : ( 80 - <Layout> 81 - {state.activeStep === 'profile' && <StepProfile />} 82 - {state.activeStep === 'interests' && <StepInterests />} 83 - {state.activeStep === 'suggested-accounts' && ( 84 - <StepSuggestedAccounts /> 85 - )} 86 - {state.activeStep === 'suggested-starterpacks' && ( 87 - <StepSuggestedStarterpacks /> 88 - )} 89 - {state.activeStep === 'find-contacts-intro' && ( 90 - <StepFindContactsIntro /> 91 - )} 92 - {state.activeStep === 'finished' && <StepFinished />} 93 - </Layout> 94 - )} 41 + <Layout> 42 + {state.activeStep === 'profile' && <StepProfile />} 43 + {state.activeStep === 'interests' && <StepInterests />} 44 + {state.activeStep === 'finished' && <StepFinished />} 45 + </Layout> 95 46 </ScreenTransition> 96 47 </Context.Provider> 97 48 </OnboardingHeaderSlot.Provider>
+5 -45
src/screens/Onboarding/state.ts
··· 6 6 type Emoji, 7 7 } from '#/screens/Onboarding/StepProfile/types' 8 8 9 - type OnboardingScreen = 10 - | 'profile' 11 - | 'interests' 12 - | 'suggested-accounts' 13 - | 'suggested-starterpacks' 14 - | 'find-contacts-intro' 15 - | 'find-contacts' 16 - | 'finished' 9 + type OnboardingScreen = 'profile' | 'interests' | 'finished' 17 10 18 11 export type OnboardingState = { 19 12 screens: Record<OnboardingScreen, boolean> ··· 49 42 type: 'prev' 50 43 } 51 44 | { 52 - type: 'skip-contacts' 53 - } 54 - | { 55 45 type: 'finish' 56 46 } 57 47 | { ··· 72 62 | undefined 73 63 } 74 64 75 - export function createInitialOnboardingState( 76 - { 77 - starterPacksStepEnabled, 78 - findContactsStepEnabled, 79 - }: { 80 - starterPacksStepEnabled: boolean 81 - findContactsStepEnabled: boolean 82 - } = {starterPacksStepEnabled: true, findContactsStepEnabled: false}, 83 - ): OnboardingState { 65 + export function createInitialOnboardingState(): OnboardingState { 84 66 const screens: OnboardingState['screens'] = { 85 67 profile: true, 86 68 interests: true, 87 - 'suggested-accounts': true, 88 - 'suggested-starterpacks': starterPacksStepEnabled, 89 - 'find-contacts-intro': findContactsStepEnabled, 90 - 'find-contacts': findContactsStepEnabled, 91 69 finished: true, 92 70 } 93 71 ··· 140 118 next.stepTransitionDirection = 'Backward' 141 119 break 142 120 } 143 - case 'skip-contacts': { 144 - const nextIndex = stepOrder.indexOf('find-contacts') + 1 145 - const nextStep = stepOrder[nextIndex] ?? 'finished' 146 - next.activeStep = nextStep 147 - next.stepTransitionDirection = 'Forward' 148 - break 149 - } 150 121 case 'finish': { 151 - next = createInitialOnboardingState({ 152 - starterPacksStepEnabled: s.screens['suggested-starterpacks'], 153 - findContactsStepEnabled: s.screens['find-contacts'], 154 - }) 122 + next = createInitialOnboardingState() 155 123 break 156 124 } 157 125 case 'setInterestsStepResults': { ··· 197 165 return [ 198 166 s.screens.profile && ('profile' as const), 199 167 s.screens.interests && ('interests' as const), 200 - s.screens['suggested-accounts'] && ('suggested-accounts' as const), 201 - s.screens['suggested-starterpacks'] && ('suggested-starterpacks' as const), 202 - s.screens['find-contacts-intro'] && ('find-contacts-intro' as const), 203 - s.screens['find-contacts'] && ('find-contacts' as const), 204 168 s.screens.finished && ('finished' as const), 205 169 ].filter(x => !!x) 206 170 } ··· 225 189 return { 226 190 state: useMemo(() => { 227 191 const stepOrder = getStepOrder(state).filter( 228 - x => x !== 'find-contacts' && x !== 'finished', 192 + x => x !== 'finished', 229 193 ) as string[] 230 194 const canGoBack = state.activeStep !== stepOrder[0] 231 195 return { ··· 235 199 * Note: for *display* purposes only, do not lean on this 236 200 * for navigation purposes! we merge certain steps! 237 201 */ 238 - activeStepIndex: stepOrder.indexOf( 239 - state.activeStep === 'find-contacts' 240 - ? 'find-contacts-intro' 241 - : state.activeStep, 242 - ), 202 + activeStepIndex: stepOrder.indexOf(state.activeStep), 243 203 totalSteps: stepOrder.length, 244 204 } 245 205 }, [state]),
+17 -8
src/screens/Profile/Header/Metrics.tsx
··· 5 5 6 6 import {makeProfileLink} from '#/lib/routes/links' 7 7 import {type Shadow} from '#/state/cache/types' 8 + import {useBskyProfileQuery} from '#/state/queries/profile' 8 9 import {formatCount} from '#/view/com/util/numeric/format' 9 10 import {atoms as a, useTheme} from '#/alf' 10 11 import {InlineLinkText} from '#/components/Link' ··· 17 18 }) { 18 19 const t = useTheme() 19 20 const {_, i18n} = useLingui() 20 - const following = formatCount(i18n, profile.followsCount || 0) 21 - const followers = formatCount(i18n, profile.followersCount || 0) 22 - const pluralizedFollowers = plural(profile.followersCount || 0, { 21 + const {data: bskyProfile} = useBskyProfileQuery({did: profile.did}) 22 + 23 + // Prefer Bluesky counts, fall back to local when user is suspended on Bluesky 24 + const followsCount = bskyProfile?.followsCount ?? profile.followsCount ?? 0 25 + const followersCount = 26 + bskyProfile?.followersCount ?? profile.followersCount ?? 0 27 + const postsCount = bskyProfile?.postsCount ?? profile.postsCount ?? 0 28 + 29 + const following = formatCount(i18n, followsCount) 30 + const followers = formatCount(i18n, followersCount) 31 + const pluralizedFollowers = plural(followersCount, { 23 32 one: 'follower', 24 33 other: 'followers', 25 34 }) 26 - const pluralizedFollowings = plural(profile.followsCount || 0, { 35 + const pluralizedFollowings = plural(followsCount, { 27 36 one: 'following', 28 37 other: 'following', 29 38 }) ··· 36 45 testID="profileHeaderFollowersButton" 37 46 style={[a.flex_row, t.atoms.text]} 38 47 to={makeProfileLink(profile, 'followers')} 39 - label={`${profile.followersCount || 0} ${pluralizedFollowers}`}> 48 + label={`${followersCount} ${pluralizedFollowers}`}> 40 49 <Text style={[a.font_semi_bold, a.text_md]}>{followers} </Text> 41 50 <Text style={[t.atoms.text_contrast_medium, a.text_md]}> 42 51 {pluralizedFollowers} ··· 46 55 testID="profileHeaderFollowsButton" 47 56 style={[a.flex_row, t.atoms.text]} 48 57 to={makeProfileLink(profile, 'follows')} 49 - label={_(msg`${profile.followsCount || 0} following`)}> 58 + label={_(msg`${followsCount} following`)}> 50 59 <Text style={[a.font_semi_bold, a.text_md]}>{following} </Text> 51 60 <Text style={[t.atoms.text_contrast_medium, a.text_md]}> 52 61 {pluralizedFollowings} 53 62 </Text> 54 63 </InlineLinkText> 55 64 <Text style={[a.font_semi_bold, t.atoms.text, a.text_md]}> 56 - {formatCount(i18n, profile.postsCount || 0)}{' '} 65 + {formatCount(i18n, postsCount)}{' '} 57 66 <Text style={[t.atoms.text_contrast_medium, a.font_normal, a.text_md]}> 58 - {plural(profile.postsCount || 0, {one: 'post', other: 'posts'})} 67 + {plural(postsCount, {one: 'post', other: 'posts'})} 59 68 </Text> 60 69 </Text> 61 70 </View>
+15 -2
src/screens/Profile/Header/ProfileHeaderStandard.tsx
··· 17 17 import {logger} from '#/logger' 18 18 import {type Shadow, useProfileShadow} from '#/state/cache/profile-shadow' 19 19 import { 20 + useBskyProfileQuery, 20 21 useProfileBlockMutationQueue, 21 22 useProfileFollowMutationQueue, 22 23 } from '#/state/queries/profile' ··· 92 93 } 93 94 94 95 const isMe = currentAccount?.did === profile.did 96 + const {data: bskyProfile} = useBskyProfileQuery({did: profile.did}) 97 + 98 + // Prefer Bluesky known followers, fall back to local 99 + const knownFollowers = 100 + bskyProfile?.viewer?.knownFollowers ?? profile.viewer?.knownFollowers 95 101 96 102 const {isActive: live} = useActorStatus(profile) 97 103 ··· 170 176 171 177 {!isMe && 172 178 !isBlockedUser && 173 - shouldShowKnownFollowers(profile.viewer?.knownFollowers) && ( 179 + shouldShowKnownFollowers(knownFollowers) && ( 174 180 <View style={[a.flex_row, a.align_center, a.gap_sm]}> 175 181 <KnownFollowers 176 - profile={profile} 182 + profile={ 183 + knownFollowers !== profile.viewer?.knownFollowers 184 + ? { 185 + ...profile, 186 + viewer: {...profile.viewer, knownFollowers}, 187 + } 188 + : profile 189 + } 177 190 moderationOpts={moderationOpts} 178 191 /> 179 192 </View>
+2 -91
src/screens/Profile/Header/Shell.tsx
··· 10 10 import {useSafeAreaInsets} from 'react-native-safe-area-context' 11 11 import {type AppBskyActorDefs, type ModerationDecision} from '@atproto/api' 12 12 import {utils} from '@bsky.app/alf' 13 - import {msg, Trans} from '@lingui/macro' 13 + import {msg} from '@lingui/macro' 14 14 import {useLingui} from '@lingui/react' 15 15 import {useNavigation} from '@react-navigation/native' 16 16 ··· 21 21 import {type NavigationProp} from '#/lib/routes/types' 22 22 import {type Shadow} from '#/state/cache/types' 23 23 import {useLightboxControls} from '#/state/lightbox' 24 - import {usePublicProfileQuery} from '#/state/queries/profile' 25 24 import {useSession} from '#/state/session' 26 25 import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' 27 26 import {UserAvatar} from '#/view/com/util/UserAvatar' 28 27 import {UserBanner} from '#/view/com/util/UserBanner' 29 28 import {atoms as a, platform, useTheme} from '#/alf' 30 - import {colors} from '#/components/Admonition' 31 29 import {Button} from '#/components/Button' 32 30 import {useDialogControl} from '#/components/Dialog' 33 31 import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeftIcon} from '#/components/icons/Arrow' 34 - import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as ArrowRotateIcon} from '#/components/icons/ArrowRotate' 35 32 import {EditLiveDialog} from '#/components/live/EditLiveDialog' 36 33 import {LiveIndicator} from '#/components/live/LiveIndicator' 37 34 import {LiveStatusDialog} from '#/components/live/LiveStatusDialog' 38 35 import {LabelsOnMe} from '#/components/moderation/LabelsOnMe' 39 36 import * as Pills from '#/components/Pills' 40 - import * as Prompt from '#/components/Prompt' 41 - import {Text} from '#/components/Typography' 42 37 import {useAnalytics} from '#/analytics' 43 38 import {IS_IOS} from '#/env' 44 39 import {GrowableAvatar} from './GrowableAvatar' ··· 310 305 export {ProfileHeaderShell} 311 306 312 307 function ProfileHeaderPills({ 313 - profile, 314 308 moderation, 315 309 }: { 316 310 profile: Shadow<AppBskyActorDefs.ProfileViewDetailed> 317 311 moderation: ModerationDecision 318 312 }) { 319 313 const modui = moderation.ui('profileView') 320 - const {data: publicProfile} = usePublicProfileQuery({did: profile.did}) 321 - 322 - const isPartiallyBackfilled = useMemo(() => { 323 - if (!publicProfile) return false 324 - const THRESHOLD_PCT = 0.05 325 - const THRESHOLD_ABS = 10 326 - const isLower = (local: number, canonical: number) => { 327 - const diff = canonical - local 328 - return diff > THRESHOLD_ABS && diff > canonical * THRESHOLD_PCT 329 - } 330 - return ( 331 - isLower(profile.followersCount ?? 0, publicProfile.followersCount ?? 0) || 332 - isLower(profile.followsCount ?? 0, publicProfile.followsCount ?? 0) || 333 - isLower(profile.postsCount ?? 0, publicProfile.postsCount ?? 0) 334 - ) 335 - }, [profile, publicProfile]) 336 314 337 315 const hasAlerts = modui.alert || modui.inform 338 - if (!isPartiallyBackfilled && !hasAlerts) return null 316 + if (!hasAlerts) return null 339 317 340 318 return ( 341 319 <Pills.Row ··· 346 324 a.pb_sm, 347 325 IS_IOS ? a.pointer_events_auto : {pointerEvents: 'box-none'}, 348 326 ]}> 349 - {isPartiallyBackfilled && <BackfillPill />} 350 327 {modui.alerts.filter(unique).map(cause => ( 351 328 <Pills.Label 352 329 size="lg" ··· 364 341 </Pills.Row> 365 342 ) 366 343 } 367 - 368 - function BackfillPill() { 369 - const t = useTheme() 370 - const {_} = useLingui() 371 - const control = Prompt.usePromptControl() 372 - 373 - return ( 374 - <> 375 - <Button 376 - label={_(msg`Backfill in progress`)} 377 - onPress={e => { 378 - e.preventDefault() 379 - e.stopPropagation() 380 - control.open() 381 - }}> 382 - {({hovered, pressed}) => ( 383 - <View 384 - style={[ 385 - a.flex_row, 386 - a.align_center, 387 - a.rounded_full, 388 - t.atoms.bg_contrast_25, 389 - (hovered || pressed) && t.atoms.bg_contrast_50, 390 - { 391 - gap: 5, 392 - paddingHorizontal: 5, 393 - paddingVertical: 5, 394 - }, 395 - ]}> 396 - <ArrowRotateIcon width={16} fill={colors.warning} /> 397 - <Text 398 - style={[ 399 - a.text_sm, 400 - a.font_semi_bold, 401 - a.leading_tight, 402 - t.atoms.text_contrast_medium, 403 - {paddingRight: 3}, 404 - ]}> 405 - <Trans>Backfill in progress</Trans> 406 - </Text> 407 - </View> 408 - )} 409 - </Button> 410 - 411 - <Prompt.Outer control={control}> 412 - <Prompt.Content> 413 - <Prompt.TitleText> 414 - <Trans>Backfill in progress</Trans> 415 - </Prompt.TitleText> 416 - <Prompt.DescriptionText> 417 - <Trans> 418 - Blacksky is still indexing this account's data from the AT 419 - Protocol network. The follower, following, and post counts shown 420 - may be lower than the actual totals. Relationship indicators (like 421 - whether this account follows you) may also be incomplete until 422 - backfill is finished. 423 - </Trans> 424 - </Prompt.DescriptionText> 425 - </Prompt.Content> 426 - <Prompt.Actions> 427 - <Prompt.Action cta={_(msg`Okay`)} onPress={() => {}} /> 428 - </Prompt.Actions> 429 - </Prompt.Outer> 430 - </> 431 - ) 432 - }
+30
src/state/queries/profile.ts
··· 30 30 import * as userActionHistory from '#/state/userActionHistory' 31 31 import {useAnalytics} from '#/analytics' 32 32 import {type Metrics, toClout} from '#/analytics/metrics' 33 + import {ALT_PROXY_DID} from '#/env' 33 34 import type * as bsky from '#/types/bsky' 34 35 import { 35 36 ProgressGuideAction, ··· 109 110 ) 110 111 if (!res.ok) return null 111 112 return res.json() 113 + }, 114 + enabled: !!did, 115 + }) 116 + } 117 + 118 + const BSKY_PROXY_HEADER = { 119 + 'atproto-proxy': `${ALT_PROXY_DID}#bsky_appview`, 120 + } 121 + 122 + /** 123 + * Fetches a profile from Bluesky's appview via the PDS proxy. 124 + * Returns null if the user is suspended on Bluesky or the request fails. 125 + * Used to get accurate follower/following counts and known followers data. 126 + */ 127 + export function useBskyProfileQuery({did}: {did: string | undefined}) { 128 + const agent = useAgent() 129 + return useQuery<AppBskyActorDefs.ProfileViewDetailed | null>({ 130 + staleTime: STALE.MINUTES.FIVE, 131 + queryKey: ['bsky-profile', did ?? ''], 132 + queryFn: async () => { 133 + try { 134 + const res = await agent.app.bsky.actor.getProfile( 135 + {actor: did ?? ''}, 136 + {headers: BSKY_PROXY_HEADER}, 137 + ) 138 + return res.data 139 + } catch { 140 + return null 141 + } 112 142 }, 113 143 enabled: !!did, 114 144 })