this repo has no description
0
fork

Configure Feed

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

Remove getSuggestedFollowsByActor fallbacks and use recIdStr (#9988)

authored by

DS Boyce and committed by
GitHub
0b6ff800 35cb2bcf

+148 -336
+45 -122
src/components/FeedInterstitials.tsx
··· 12 12 import {useLingui} from '@lingui/react' 13 13 import {Trans} from '@lingui/react/macro' 14 14 import {useNavigation} from '@react-navigation/native' 15 + import {useQueryClient} from '@tanstack/react-query' 15 16 16 17 import {type NavigationProp} from '#/lib/routes/types' 17 18 import {useModerationOpts} from '#/state/preferences/moderation-opts' ··· 19 20 import {type FeedDescriptor} from '#/state/queries/post-feed' 20 21 import {useProfilesQuery} from '#/state/queries/profile' 21 22 import { 23 + suggestedFollowsByActorQueryKey, 22 24 useSuggestedFollowsByActorQuery, 23 - useSuggestedFollowsQuery, 24 25 } from '#/state/queries/suggested-follows' 25 26 import {useSession} from '#/state/session' 26 27 import * as userActionHistory from '#/state/userActionHistory' ··· 170 171 if (followSuggestions.length > 0) { 171 172 suggestedDids = [ 172 173 // It's ok if these will pick the same item (weighed by its frequency) 174 + /* eslint-disable react-hooks/purity */ 173 175 followSuggestions[Math.floor(Math.random() * followSuggestions.length)], 174 176 followSuggestions[Math.floor(Math.random() * followSuggestions.length)], 175 177 followSuggestions[Math.floor(Math.random() * followSuggestions.length)], 176 178 followSuggestions[Math.floor(Math.random() * followSuggestions.length)], 179 + /* eslint-enable react-hooks/purity */ 177 180 ] 178 181 } 179 182 const seenDids = seen ··· 216 219 } 217 220 218 221 export function SuggestedFollowsProfile({did}: {did: string}) { 219 - const {gtMobile} = useBreakpoints() 220 - const moderationOpts = useModerationOpts() 221 - const maxLength = gtMobile ? 4 : 6 222 222 const { 223 223 isLoading: isSuggestionsLoading, 224 224 data, ··· 226 226 } = useSuggestedFollowsByActorQuery({ 227 227 did, 228 228 }) 229 - const { 230 - data: moreSuggestions, 231 - fetchNextPage, 232 - hasNextPage, 233 - isFetchingNextPage, 234 - } = useSuggestedFollowsQuery({limit: 25}) 229 + const queryClient = useQueryClient() 235 230 236 - const [dismissedDids, setDismissedDids] = useState<Set<string>>(new Set()) 231 + const onDismiss = useCallback( 232 + (dismissedDid: string) => { 233 + queryClient.setQueryData( 234 + suggestedFollowsByActorQueryKey(did), 235 + (previous: typeof data) => { 236 + if (!previous) return previous 237 + return { 238 + ...previous, 239 + suggestions: previous.suggestions.filter( 240 + s => s.did !== dismissedDid, 241 + ), 242 + } 243 + }, 244 + ) 245 + }, 246 + [did, queryClient], 247 + ) 237 248 238 - const onDismiss = useCallback((dismissedDid: string) => { 239 - setDismissedDids(prev => new Set(prev).add(dismissedDid)) 240 - }, []) 241 - 242 - // Combine profiles from the actor-specific query with fallback suggestions 243 - const allProfiles = useMemo(() => { 244 - const actorProfiles = data?.suggestions ?? [] 245 - const fallbackProfiles = 246 - moreSuggestions?.pages.flatMap(page => 247 - page.actors.map(actor => ({actor, recId: page.recId})), 248 - ) ?? [] 249 - 250 - // Dedupe by did, preferring actor-specific profiles 251 - const seen = new Set<string>() 252 - const combined: {actor: bsky.profile.AnyProfileView; recId?: number}[] = [] 253 - 254 - for (const profile of actorProfiles) { 255 - if (!seen.has(profile.did)) { 256 - seen.add(profile.did) 257 - combined.push({actor: profile, recId: data?.recId}) 258 - } 259 - } 260 - 261 - for (const profile of fallbackProfiles) { 262 - if (!seen.has(profile.actor.did) && profile.actor.did !== did) { 263 - seen.add(profile.actor.did) 264 - combined.push(profile) 265 - } 266 - } 267 - 268 - return combined 269 - }, [data?.suggestions, moreSuggestions?.pages, did, data?.recId]) 270 - 271 - const filteredProfiles = useMemo(() => { 272 - return allProfiles.filter(p => !dismissedDids.has(p.actor.did)) 273 - }, [allProfiles, dismissedDids]) 274 - 275 - // Fetch more when running low 276 - useEffect(() => { 277 - if ( 278 - moderationOpts && 279 - filteredProfiles.length < maxLength && 280 - hasNextPage && 281 - !isFetchingNextPage 282 - ) { 283 - void fetchNextPage() 284 - } 285 - }, [ 286 - filteredProfiles.length, 287 - maxLength, 288 - hasNextPage, 289 - isFetchingNextPage, 290 - fetchNextPage, 291 - moderationOpts, 292 - ]) 249 + const profiles = useMemo(() => { 250 + return (data?.suggestions ?? []).map(profile => ({ 251 + actor: profile, 252 + recId: data?.recId, 253 + })) 254 + }, [data?.suggestions, data?.recId]) 293 255 294 256 return ( 295 257 <ProfileGrid 296 258 isSuggestionsLoading={isSuggestionsLoading} 297 - profiles={filteredProfiles} 298 - totalProfileCount={allProfiles.length} 259 + profiles={profiles} 299 260 error={error} 300 261 viewContext="profile" 301 262 onDismiss={onDismiss} ··· 304 265 } 305 266 306 267 export function SuggestedFollowsHome() { 307 - const {gtMobile} = useBreakpoints() 308 - const moderationOpts = useModerationOpts() 309 - const maxLength = gtMobile ? 4 : 6 310 268 const { 311 269 isLoading: isSuggestionsLoading, 312 270 profiles: experimentalProfiles, 313 271 error: experimentalError, 314 272 } = useExperimentalSuggestedUsersQuery() 315 - const { 316 - data: moreSuggestions, 317 - fetchNextPage, 318 - hasNextPage, 319 - isFetchingNextPage, 320 - error: suggestionsError, 321 - } = useSuggestedFollowsQuery({limit: 25}) 322 273 323 274 const [dismissedDids, setDismissedDids] = useState<Set<string>>(new Set()) 324 275 ··· 326 277 setDismissedDids(prev => new Set(prev).add(did)) 327 278 }, []) 328 279 329 - // Combine profiles from experimental query with paginated suggestions 330 280 const allProfiles = useMemo(() => { 331 - const fallbackProfiles = 332 - moreSuggestions?.pages.flatMap(page => 333 - page.actors.map(actor => ({actor, recId: page.recId})), 334 - ) ?? [] 335 - 336 - // Dedupe by did, preferring experimental profiles 337 - const seen = new Set<string>() 338 - const combined: Array<{ 281 + const result: Array<{ 339 282 actor: bsky.profile.AnyProfileView 340 - recId?: number 283 + recId?: string 341 284 }> = [] 342 285 343 286 for (const profile of experimentalProfiles) { 344 - if (!seen.has(profile.did)) { 345 - seen.add(profile.did) 346 - combined.push({actor: profile, recId: undefined}) 347 - } 287 + result.push({actor: profile, recId: undefined}) 348 288 } 349 289 350 - for (const profile of fallbackProfiles) { 351 - if (!seen.has(profile.actor.did)) { 352 - seen.add(profile.actor.did) 353 - combined.push(profile) 354 - } 355 - } 356 - 357 - return combined 358 - }, [experimentalProfiles, moreSuggestions?.pages]) 290 + return result 291 + }, [experimentalProfiles]) 359 292 360 293 const filteredProfiles = useMemo(() => { 361 294 return allProfiles.filter(p => !dismissedDids.has(p.actor.did)) 362 295 }, [allProfiles, dismissedDids]) 363 296 364 - // Fetch more when running low 365 - useEffect(() => { 366 - if ( 367 - moderationOpts && 368 - filteredProfiles.length < maxLength && 369 - hasNextPage && 370 - !isFetchingNextPage 371 - ) { 372 - void fetchNextPage() 373 - } 374 - }, [ 375 - filteredProfiles.length, 376 - maxLength, 377 - hasNextPage, 378 - isFetchingNextPage, 379 - fetchNextPage, 380 - moderationOpts, 381 - ]) 382 - 383 297 return ( 384 298 <ProfileGrid 385 299 isSuggestionsLoading={isSuggestionsLoading} 386 300 profiles={filteredProfiles} 387 301 totalProfileCount={allProfiles.length} 388 - error={experimentalError || suggestionsError} 302 + error={experimentalError} 389 303 viewContext="feed" 390 304 onDismiss={onDismiss} 391 305 /> ··· 400 314 viewContext = 'feed', 401 315 onDismiss, 402 316 isVisible = true, 317 + onRequestHide, 403 318 }: { 404 319 isSuggestionsLoading: boolean 405 - profiles: {actor: bsky.profile.AnyProfileView; recId?: number}[] 320 + profiles: {actor: bsky.profile.AnyProfileView; recId?: string}[] 406 321 totalProfileCount?: number 407 322 error: Error | null 408 323 viewContext: 'profile' | 'profileHeader' | 'feed' 409 324 onDismiss?: (did: string) => void 410 325 isVisible?: boolean 326 + onRequestHide?: () => void 411 327 }) { 412 328 const t = useTheme() 413 329 const ax = useAnalytics() ··· 651 567 652 568 // Use totalProfileCount (before dismissals) for minLength check on initial render. 653 569 const profileCountForMinCheck = totalProfileCount ?? profiles.length 570 + 571 + useEffect(() => { 572 + if (error || (!isLoading && profileCountForMinCheck < minLength)) { 573 + onRequestHide?.() 574 + } 575 + }, [error, isLoading, onRequestHide, profileCountForMinCheck, minLength]) 576 + 654 577 if (error || (!isLoading && profileCountForMinCheck < minLength)) { 655 578 ax.logger.debug(`Not enough profiles to show suggested follows`) 656 579 return null
+23 -7
src/screens/Profile/Header/ProfileHeaderStandard.tsx
··· 75 75 const [, queueUnblock] = useProfileBlockMutationQueue(profile) 76 76 const unblockPromptControl = Prompt.usePromptControl() 77 77 const [showSuggestedFollows, setShowSuggestedFollows] = useState(false) 78 + const [hasSeenAllSuggestedFollows, setHasSeenAllSuggestedFollows] = 79 + useState(false) 78 80 const isBlockedUser = 79 81 profile.viewer?.blocking || 80 82 profile.viewer?.blockedBy || ··· 84 86 try { 85 87 await queueUnblock() 86 88 Toast.show(_(msg({message: 'Account unblocked', context: 'toast'}))) 87 - } catch (e: any) { 89 + } catch (err) { 90 + const e = err as Error 88 91 if (e?.name !== 'AbortError') { 89 92 logger.error('Failed to unblock account', {message: e}) 90 93 Toast.show(_(msg`There was an issue! ${e.toString()}`), {type: 'error'}) 91 94 } 92 95 } 96 + } 97 + 98 + const onRequestHide = () => { 99 + setHasSeenAllSuggestedFollows(true) 100 + setShowSuggestedFollows(false) 93 101 } 94 102 95 103 const isMe = currentAccount?.did === profile.did ··· 192 200 description={_( 193 201 msg`The account will be able to interact with you after unblocking.`, 194 202 )} 195 - onConfirm={unblockAccount} 203 + onConfirm={() => { 204 + void unblockAccount() 205 + }} 196 206 confirmButtonCta={ 197 207 profile.viewer?.blocking ? _(msg`Unblock`) : _(msg`Block`) 198 208 } ··· 201 211 </ProfileHeaderShell> 202 212 203 213 <ProfileHeaderSuggestedFollows 204 - isExpanded={showSuggestedFollows} 214 + isExpanded={!hasSeenAllSuggestedFollows && showSuggestedFollows} 205 215 actorDid={profile.did} 216 + onRequestHide={onRequestHide} 206 217 /> 207 218 </> 208 219 ) ··· 254 265 )}`, 255 266 ), 256 267 ) 257 - } catch (e: any) { 268 + } catch (err) { 269 + const e = err as Error 258 270 if (e?.name !== 'AbortError') { 259 271 logger.error('Failed to follow', {message: String(e)}) 260 272 Toast.show(_(msg`There was an issue! ${e.toString()}`), { ··· 280 292 ), 281 293 {type: 'default'}, 282 294 ) 283 - } catch (e: any) { 295 + } catch (err) { 296 + const e = err as Error 284 297 if (e?.name !== 'AbortError') { 285 298 logger.error('Failed to unfollow', {message: String(e)}) 286 299 Toast.show(_(msg`There was an issue! ${e.toString()}`), { ··· 295 308 try { 296 309 await queueUnblock() 297 310 Toast.show(_(msg({message: 'Account unblocked', context: 'toast'}))) 298 - } catch (e: any) { 311 + } catch (err) { 312 + const e = err as Error 299 313 if (e?.name !== 'AbortError') { 300 314 logger.error('Failed to unblock account', {message: e}) 301 315 Toast.show(_(msg`There was an issue! ${e.toString()}`), {type: 'error'}) ··· 400 414 description={_( 401 415 msg`The account will be able to interact with you after unblocking.`, 402 416 )} 403 - onConfirm={unblockAccount} 417 + onConfirm={() => { 418 + void unblockAccount() 419 + }} 404 420 confirmButtonCta={_(msg`Unblock`)} 405 421 confirmButtonColor="negative" 406 422 />
+34 -76
src/screens/Profile/Header/SuggestedFollows.tsx
··· 1 - import {useCallback, useEffect, useMemo, useState} from 'react' 1 + import {useCallback, useMemo} from 'react' 2 + import {useQueryClient} from '@tanstack/react-query' 2 3 3 4 import {AccordionAnimation} from '#/lib/custom-animations/AccordionAnimation' 4 - import {useModerationOpts} from '#/state/preferences/moderation-opts' 5 5 import { 6 + suggestedFollowsByActorQueryKey, 6 7 useSuggestedFollowsByActorQuery, 7 - useSuggestedFollowsQuery, 8 8 } from '#/state/queries/suggested-follows' 9 - import {useBreakpoints} from '#/alf' 10 9 import {ProfileGrid} from '#/components/FeedInterstitials' 11 10 import {IS_ANDROID} from '#/env' 12 11 import type * as bsky from '#/types/bsky' ··· 14 13 export function ProfileHeaderSuggestedFollows({ 15 14 isExpanded, 16 15 actorDid, 16 + onRequestHide, 17 17 }: { 18 18 isExpanded: boolean 19 19 actorDid: string 20 + onRequestHide: () => void 20 21 }) { 21 - const {allProfiles, filteredProfiles, onDismiss, isLoading, error} = 22 + const {profiles, onDismiss, isLoading, error} = 22 23 useProfileHeaderSuggestions(actorDid) 23 - 24 - if (!allProfiles.length && !isLoading) return null 25 24 26 25 /* NOTE (caidanw): 27 26 * Android does not work well with this feature yet. ··· 34 33 <AccordionAnimation isExpanded={isExpanded}> 35 34 <ProfileGrid 36 35 isSuggestionsLoading={isLoading} 37 - profiles={filteredProfiles} 38 - totalProfileCount={allProfiles.length} 36 + profiles={profiles} 37 + totalProfileCount={profiles.length} 39 38 error={error} 40 39 viewContext="profileHeader" 41 40 onDismiss={onDismiss} 42 41 isVisible={isExpanded} 42 + onRequestHide={onRequestHide} 43 43 /> 44 44 </AccordionAnimation> 45 45 ) 46 46 } 47 47 48 48 function useProfileHeaderSuggestions(actorDid: string) { 49 - const {gtMobile} = useBreakpoints() 50 - const moderationOpts = useModerationOpts() 51 - const maxLength = gtMobile ? 4 : 12 52 49 const {isLoading, data, error} = useSuggestedFollowsByActorQuery({ 53 50 did: actorDid, 54 51 }) 55 - const { 56 - data: moreSuggestions, 57 - fetchNextPage, 58 - hasNextPage, 59 - isFetchingNextPage, 60 - } = useSuggestedFollowsQuery({limit: 25}) 61 - 62 - const [dismissedDids, setDismissedDids] = useState<Set<string>>(new Set()) 63 - 64 - const onDismiss = useCallback((did: string) => { 65 - setDismissedDids(prev => new Set(prev).add(did)) 66 - }, []) 67 - 68 - // Combine profiles from the actor-specific query with fallback suggestions 69 - const allProfiles = useMemo(() => { 70 - const actorProfiles = data?.suggestions ?? [] 71 - const fallbackProfiles = 72 - moreSuggestions?.pages.flatMap(page => 73 - page.actors.map(actor => ({actor, recId: page.recId})), 74 - ) ?? [] 75 - 76 - // Dedupe by did, preferring actor-specific profiles 77 - const seen = new Set<string>() 78 - const combined: {actor: bsky.profile.AnyProfileView; recId?: number}[] = [] 79 - 80 - for (const profile of actorProfiles) { 81 - if (!seen.has(profile.did)) { 82 - seen.add(profile.did) 83 - combined.push({actor: profile, recId: data?.recId}) 84 - } 85 - } 86 - 87 - for (const profile of fallbackProfiles) { 88 - if (!seen.has(profile.actor.did) && profile.actor.did !== actorDid) { 89 - seen.add(profile.actor.did) 90 - combined.push(profile) 91 - } 92 - } 52 + const queryClient = useQueryClient() 93 53 94 - return combined 95 - }, [data?.suggestions, moreSuggestions?.pages, actorDid, data?.recId]) 54 + const onDismiss = useCallback( 55 + (dismissedDid: string) => { 56 + queryClient.setQueryData( 57 + suggestedFollowsByActorQueryKey(actorDid), 58 + (previous: typeof data) => { 59 + if (!previous) return previous 60 + return { 61 + ...previous, 62 + suggestions: previous.suggestions.filter( 63 + s => s.did !== dismissedDid, 64 + ), 65 + } 66 + }, 67 + ) 68 + }, 69 + [actorDid, queryClient], 70 + ) 96 71 97 - const filteredProfiles = useMemo(() => { 98 - return allProfiles.filter(p => !dismissedDids.has(p.actor.did)) 99 - }, [allProfiles, dismissedDids]) 100 - 101 - // Fetch more when running low 102 - useEffect(() => { 103 - if ( 104 - moderationOpts && 105 - filteredProfiles.length < maxLength && 106 - hasNextPage && 107 - !isFetchingNextPage 108 - ) { 109 - void fetchNextPage() 110 - } 111 - }, [ 112 - filteredProfiles.length, 113 - maxLength, 114 - hasNextPage, 115 - isFetchingNextPage, 116 - fetchNextPage, 117 - moderationOpts, 118 - ]) 72 + const profiles = useMemo(() => { 73 + return (data?.suggestions ?? []).map(profile => ({ 74 + actor: profile as bsky.profile.AnyProfileView, 75 + recId: data?.recId, 76 + })) 77 + }, [data?.suggestions, data?.recId]) 119 78 120 79 return { 121 - allProfiles, 122 - filteredProfiles, 80 + profiles, 123 81 onDismiss, 124 82 isLoading, 125 83 error,
+13 -13
src/state/queries/profile.ts
··· 5 5 type AppBskyActorGetProfiles, 6 6 type AppBskyActorProfile, 7 7 type AppBskyGraphGetFollows, 8 + type AtpAgent, 8 9 AtUri, 9 - type BskyAgent, 10 10 type ComAtprotoRepoUploadBlob, 11 11 type Un$Typed, 12 12 } from '@atproto/api' ··· 203 203 (res => { 204 204 if (typeof newUserAvatar !== 'undefined') { 205 205 if (newUserAvatar === null && res.data.avatar) { 206 - // url hasnt cleared yet 206 + // url hasn't cleared yet 207 207 return false 208 208 } else if (res.data.avatar === profile.avatar) { 209 - // url hasnt changed yet 209 + // url hasn't changed yet 210 210 return false 211 211 } 212 212 } 213 213 if (typeof newUserBanner !== 'undefined') { 214 214 if (newUserBanner === null && res.data.banner) { 215 - // url hasnt cleared yet 215 + // url hasn't cleared yet 216 216 return false 217 217 } else if (res.data.banner === profile.banner) { 218 - // url hasnt changed yet 218 + // url hasn't changed yet 219 219 return false 220 220 } 221 221 } ··· 231 231 }, 232 232 async onSuccess(_, variables) { 233 233 // invalidate cache 234 - queryClient.invalidateQueries({ 234 + void queryClient.invalidateQueries({ 235 235 queryKey: RQKEY(variables.profile.did), 236 236 }) 237 - queryClient.invalidateQueries({ 237 + void queryClient.invalidateQueries({ 238 238 queryKey: [profilesQueryKeyRoot, [variables.profile.did]], 239 239 }) 240 240 await updateProfileVerificationCache({profile: variables.profile}) ··· 329 329 } 330 330 331 331 if (finalFollowingUri) { 332 - agent.app.bsky.graph 332 + void agent.app.bsky.graph 333 333 .getSuggestedFollowsByActor({ 334 334 actor: did, 335 335 }) ··· 474 474 await agent.mute(did) 475 475 }, 476 476 onSuccess() { 477 - queryClient.invalidateQueries({queryKey: RQKEY_MY_MUTED()}) 477 + void queryClient.invalidateQueries({queryKey: RQKEY_MY_MUTED()}) 478 478 }, 479 479 }) 480 480 } ··· 487 487 await agent.unmute(did) 488 488 }, 489 489 onSuccess() { 490 - queryClient.invalidateQueries({queryKey: RQKEY_MY_MUTED()}) 490 + void queryClient.invalidateQueries({queryKey: RQKEY_MY_MUTED()}) 491 491 }, 492 492 }) 493 493 } ··· 527 527 updateProfileShadow(queryClient, did, { 528 528 blockingUri: finalBlockingUri, 529 529 }) 530 - queryClient.invalidateQueries({queryKey: [RQKEY_LIST_CONVOS]}) 530 + void queryClient.invalidateQueries({queryKey: [RQKEY_LIST_CONVOS]}) 531 531 }, 532 532 }) 533 533 ··· 565 565 ) 566 566 }, 567 567 onSuccess(_, {did}) { 568 - queryClient.invalidateQueries({queryKey: RQKEY_MY_BLOCKED()}) 568 + void queryClient.invalidateQueries({queryKey: RQKEY_MY_BLOCKED()}) 569 569 resetProfilePostsQueries(queryClient, did, 1000) 570 570 }, 571 571 }) ··· 593 593 } 594 594 595 595 async function whenAppViewReady( 596 - agent: BskyAgent, 596 + agent: AtpAgent, 597 597 actor: string, 598 598 fn: (res: AppBskyActorGetProfile.Response) => boolean, 599 599 ) {
+6 -89
src/state/queries/suggested-follows.ts
··· 2 2 type AppBskyActorDefs, 3 3 type AppBskyActorGetSuggestions, 4 4 type AppBskyGraphGetSuggestedFollowsByActor, 5 - moderateProfile, 6 5 } from '@atproto/api' 7 6 import { 8 7 type InfiniteData, 9 8 type QueryClient, 10 - type QueryKey, 11 - useInfiniteQuery, 12 9 useQuery, 13 10 } from '@tanstack/react-query' 14 11 15 - import { 16 - aggregateUserInterests, 17 - createBskyTopicsHeader, 18 - } from '#/lib/api/feed/utils' 19 - import {getContentLanguages} from '#/state/preferences/languages' 20 12 import {STALE} from '#/state/queries' 21 - import {usePreferencesQuery} from '#/state/queries/preferences' 22 - import {useAgent, useSession} from '#/state/session' 23 - import {useModerationOpts} from '../preferences/moderation-opts' 13 + import {useAgent} from '#/state/session' 24 14 25 15 const suggestedFollowsQueryKeyRoot = 'suggested-follows' 26 - const suggestedFollowsQueryKey = (options?: SuggestedFollowsOptions) => [ 27 - suggestedFollowsQueryKeyRoot, 28 - options, 29 - ] 30 16 31 17 const suggestedFollowsByActorQueryKeyRoot = 'suggested-follows-by-actor' 32 - const suggestedFollowsByActorQueryKey = (did: string) => [ 18 + export const suggestedFollowsByActorQueryKey = (did: string) => [ 33 19 suggestedFollowsByActorQueryKeyRoot, 34 20 did, 35 21 ] 36 22 37 - type SuggestedFollowsOptions = {limit?: number; subsequentPageLimit?: number} 38 - 39 - export function useSuggestedFollowsQuery(options?: SuggestedFollowsOptions) { 40 - const {currentAccount} = useSession() 41 - const agent = useAgent() 42 - const moderationOpts = useModerationOpts() 43 - const {data: preferences} = usePreferencesQuery() 44 - const limit = options?.limit || 25 45 - 46 - return useInfiniteQuery< 47 - AppBskyActorGetSuggestions.OutputSchema, 48 - Error, 49 - InfiniteData<AppBskyActorGetSuggestions.OutputSchema>, 50 - QueryKey, 51 - string | undefined 52 - >({ 53 - enabled: !!moderationOpts && !!preferences, 54 - staleTime: STALE.HOURS.ONE, 55 - queryKey: suggestedFollowsQueryKey(options), 56 - queryFn: async ({pageParam}) => { 57 - const contentLangs = getContentLanguages().join(',') 58 - const maybeDifferentLimit = 59 - options?.subsequentPageLimit && pageParam 60 - ? options.subsequentPageLimit 61 - : limit 62 - const res = await agent.app.bsky.actor.getSuggestions( 63 - { 64 - limit: maybeDifferentLimit, 65 - cursor: pageParam, 66 - }, 67 - { 68 - headers: { 69 - ...createBskyTopicsHeader(aggregateUserInterests(preferences)), 70 - 'Accept-Language': contentLangs, 71 - }, 72 - }, 73 - ) 74 - 75 - res.data.actors = res.data.actors 76 - .filter( 77 - actor => 78 - !moderateProfile(actor, moderationOpts!).ui('profileList').filter, 79 - ) 80 - .filter(actor => { 81 - const viewer = actor.viewer 82 - if (viewer) { 83 - if ( 84 - viewer.following || 85 - viewer.muted || 86 - viewer.mutedByList || 87 - viewer.blockedBy || 88 - viewer.blocking 89 - ) { 90 - return false 91 - } 92 - } 93 - if (actor.did === currentAccount?.did) { 94 - return false 95 - } 96 - return true 97 - }) 98 - 99 - return res.data 100 - }, 101 - initialPageParam: undefined, 102 - getNextPageParam: lastPage => lastPage.cursor, 103 - }) 104 - } 105 - 106 23 export function useSuggestedFollowsByActorQuery({ 107 24 did, 108 25 enabled, ··· 120 37 const res = await agent.app.bsky.graph.getSuggestedFollowsByActor({ 121 38 actor: did, 122 39 }) 123 - const suggestions = res.data.isFallback 124 - ? [] 125 - : res.data.suggestions.filter(profile => !profile.viewer?.following) 126 - return {suggestions, recId: res.data.recId} 40 + const suggestions = res.data.suggestions.filter( 41 + profile => !profile.viewer?.following, 42 + ) 43 + return {suggestions, recId: res.data.recIdStr} 127 44 }, 128 45 enabled, 129 46 })
+27 -29
src/view/com/posts/PostFeed.tsx
··· 15 15 AppBskyEmbedVideo, 16 16 type AppBskyFeedDefs, 17 17 } from '@atproto/api' 18 - import {msg} from '@lingui/core/macro' 19 - import {useLingui} from '@lingui/react' 18 + import {useLingui} from '@lingui/react/macro' 20 19 import {useQueryClient} from '@tanstack/react-query' 21 20 22 21 import {DISCOVER_FEED_URI, KNOWN_SHUTDOWN_FEEDS} from '#/lib/constants' ··· 226 225 savedFeedConfig?: AppBskyActorDefs.SavedFeed 227 226 initialNumToRender?: number 228 227 isVideoFeed?: boolean 228 + lastFetchDate?: () => number 229 229 }): React.ReactNode => { 230 230 const ax = useAnalytics() 231 - const {_} = useLingui() 231 + const {t: l} = useLingui() 232 232 const queryClient = useQueryClient() 233 233 const {currentAccount, hasSession} = useSession() 234 234 const initialNumToRender = useInitialNumToRender() 235 235 const feedFeedback = useFeedFeedbackContext() 236 236 const [isPTRing, setIsPTRing] = useState(false) 237 + // eslint-disable-next-line react-hooks/purity 237 238 const lastFetchRef = useRef<number>(Date.now()) 238 239 const [feedType, feedUriOrActorDid, feedTab] = feed.split('|') 239 240 const {gtMobile} = useBreakpoints() ··· 271 272 fetchNextPage, 272 273 } = usePostFeedQuery(feed, feedParams, opts) 273 274 const lastFetchedAt = data?.pages[0].fetchedAt 274 - if (lastFetchedAt) { 275 - lastFetchRef.current = lastFetchedAt 276 - } 277 275 const isEmpty = useMemo( 278 276 () => !isFetching && !data?.pages?.some(page => page.slices.length), 279 277 [isFetching, data], 280 278 ) 281 279 280 + useEffect(() => { 281 + if (lastFetchedAt) { 282 + lastFetchRef.current = lastFetchedAt 283 + } 284 + }, [lastFetchedAt]) 285 + 282 286 const checkForNew = useNonReactiveCallback(async () => { 283 287 if (!data?.pages[0] || isFetching || !onHasNew || !enabled || disablePoll) { 284 288 return ··· 292 296 try { 293 297 if (await pollLatest(data.pages[0])) { 294 298 if (isEmpty) { 295 - refetch() 299 + void refetch() 296 300 } else { 297 301 onHasNew(true) 298 302 } ··· 333 337 const timeSinceFirstLoad = Date.now() - lastFetchRef.current 334 338 if (isEmpty || timeSinceFirstLoad > CHECK_LATEST_AFTER) { 335 339 // check for new on enable (aka on focus) 336 - checkForNew() 340 + void checkForNew() 337 341 } 338 342 } 339 343 }, [enabled, isEmpty, disablePoll, checkForNew]) ··· 343 347 const subscription = AppState.addEventListener('change', nextAppState => { 344 348 // check for new on app foreground 345 349 if (nextAppState === 'active') { 346 - checkForNew() 350 + void checkForNew() 347 351 } 348 352 }) 349 353 cleanup1 = () => subscription.remove() 350 354 if (pollInterval) { 351 355 // check for new on interval 352 356 const i = setInterval(() => { 353 - checkForNew() 357 + void checkForNew() 354 358 }, pollInterval) 355 359 cleanup2 = () => clearInterval(i) 356 360 } ··· 363 367 const followProgressGuide = useProgressGuide('follow-10') 364 368 const followAndLikeProgressGuide = useProgressGuide('like-10-and-follow-7') 365 369 366 - const showProgressIntersitial = 370 + const showProgressInterstitial = 367 371 (followProgressGuide || followAndLikeProgressGuide) && !rightNavVisible 368 372 369 373 const {trendingVideoDisabled} = useTrendingSettings() ··· 494 498 if (hasSession) { 495 499 if (feedKind === 'discover') { 496 500 if (sliceIndex === 0) { 497 - if (showProgressIntersitial) { 501 + if (showProgressInterstitial) { 498 502 arr.push({ 499 503 type: 'interstitialProgressGuide', 500 504 key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt, ··· 672 676 feedUriOrActorDid, 673 677 feedTab, 674 678 hasSession, 675 - showProgressIntersitial, 679 + showProgressInterstitial, 676 680 trendingVideoDisabled, 677 681 gtMobile, 678 682 isVideoFeed, ··· 683 687 blockedOrMutedAuthors, 684 688 ]) 685 689 686 - useEffect(() => { 687 - if (enabled === false) { 688 - setIsPTRing(false) 689 - } 690 - }, [enabled]) 691 - 692 690 // events 693 691 // = 694 692 695 693 const onRefresh = useCallback(async () => { 694 + if (!enabled) return 695 + 696 696 ax.metric('feed:refresh', { 697 697 feedType: feedType, 698 698 feedUrl: feed, ··· 706 706 logger.error('Failed to refresh posts feed', {message: err}) 707 707 } 708 708 setIsPTRing(false) 709 - }, [ax, refetch, setIsPTRing, onHasNew, feed, feedType]) 709 + }, [ax, refetch, setIsPTRing, onHasNew, feed, feedType, enabled]) 710 710 711 711 const onEndReached = useCallback(async () => { 712 712 if (isFetching || !hasNextPage || isError) return ··· 733 733 ]) 734 734 735 735 const onPressTryAgain = useCallback(() => { 736 - refetch() 736 + void refetch() 737 737 onHasNew?.(false) 738 738 }, [refetch, onHasNew]) 739 739 740 740 const onPressRetryLoadMore = useCallback(() => { 741 - fetchNextPage() 741 + void fetchNextPage() 742 742 }, [fetchNextPage]) 743 743 744 744 // rendering ··· 760 760 } else if (row.type === 'loadMoreError') { 761 761 return ( 762 762 <LoadMoreRetryBtn 763 - label={_( 764 - msg`There was an issue fetching posts. Tap here to try again.`, 765 - )} 763 + label={l`There was an issue fetching posts. Tap here to try again.`} 766 764 onPress={onPressRetryLoadMore} 767 765 /> 768 766 ) ··· 861 859 error, 862 860 onPressTryAgain, 863 861 savedFeedConfig, 864 - _, 862 + l, 865 863 onPressRetryLoadMore, 866 864 feedType, 867 865 feedUriOrActorDid, ··· 997 995 testID={testID ? `${testID}-flatlist` : undefined} 998 996 ref={scrollElRef} 999 997 data={feedItems} 1000 - keyExtractor={item => item.key} 998 + keyExtractor={(item: FeedRow) => item.key} 1001 999 renderItem={renderItem} 1002 1000 ListFooterComponent={FeedFooter} 1003 1001 ListHeaderComponent={ListHeaderComponent} 1004 1002 refreshing={isPTRing} 1005 - onRefresh={onRefresh} 1003 + onRefresh={() => void onRefresh()} 1006 1004 headerOffset={headerOffset} 1007 1005 progressViewOffset={progressViewOffset} 1008 1006 contentContainerStyle={{ 1009 1007 minHeight: Dimensions.get('window').height * 1.5, 1010 1008 }} 1011 1009 onScrolledDownChange={handleScrolledDownChange} 1012 - onEndReached={onEndReached} 1010 + onEndReached={() => void onEndReached()} 1013 1011 onEndReachedThreshold={2} // number of posts left to trigger load more 1014 1012 removeClippedSubviews={true} 1015 1013 extraData={extraData}