From 0b6ff8000df6a583defbf730e58689ca7bd55d1e Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Thu, 12 Mar 2026 16:57:38 -0700 Subject: [PATCH] Remove getSuggestedFollowsByActor fallbacks and use recIdStr (#9988) --- src/components/FeedInterstitials.tsx | 167 +++++------------- .../Profile/Header/ProfileHeaderStandard.tsx | 30 +++- .../Profile/Header/SuggestedFollows.tsx | 110 ++++-------- src/state/queries/profile.ts | 26 +-- src/state/queries/suggested-follows.ts | 95 +--------- src/view/com/posts/PostFeed.tsx | 56 +++--- 6 files changed, 148 insertions(+), 336 deletions(-) diff --git a/src/components/FeedInterstitials.tsx b/src/components/FeedInterstitials.tsx index 2d070843fb..aff354ac1c 100644 --- a/src/components/FeedInterstitials.tsx +++ b/src/components/FeedInterstitials.tsx @@ -12,6 +12,7 @@ import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' import {useNavigation} from '@react-navigation/native' +import {useQueryClient} from '@tanstack/react-query' import {type NavigationProp} from '#/lib/routes/types' import {useModerationOpts} from '#/state/preferences/moderation-opts' @@ -19,8 +20,8 @@ import {useGetPopularFeedsQuery} from '#/state/queries/feed' import {type FeedDescriptor} from '#/state/queries/post-feed' import {useProfilesQuery} from '#/state/queries/profile' import { + suggestedFollowsByActorQueryKey, useSuggestedFollowsByActorQuery, - useSuggestedFollowsQuery, } from '#/state/queries/suggested-follows' import {useSession} from '#/state/session' import * as userActionHistory from '#/state/userActionHistory' @@ -170,10 +171,12 @@ function useExperimentalSuggestedUsersQuery() { if (followSuggestions.length > 0) { suggestedDids = [ // It's ok if these will pick the same item (weighed by its frequency) + /* eslint-disable react-hooks/purity */ followSuggestions[Math.floor(Math.random() * followSuggestions.length)], followSuggestions[Math.floor(Math.random() * followSuggestions.length)], followSuggestions[Math.floor(Math.random() * followSuggestions.length)], followSuggestions[Math.floor(Math.random() * followSuggestions.length)], + /* eslint-enable react-hooks/purity */ ] } const seenDids = seen @@ -216,9 +219,6 @@ export function SuggestedFollows({feed}: {feed: FeedDescriptor}) { } export function SuggestedFollowsProfile({did}: {did: string}) { - const {gtMobile} = useBreakpoints() - const moderationOpts = useModerationOpts() - const maxLength = gtMobile ? 4 : 6 const { isLoading: isSuggestionsLoading, data, @@ -226,76 +226,37 @@ export function SuggestedFollowsProfile({did}: {did: string}) { } = useSuggestedFollowsByActorQuery({ did, }) - const { - data: moreSuggestions, - fetchNextPage, - hasNextPage, - isFetchingNextPage, - } = useSuggestedFollowsQuery({limit: 25}) + const queryClient = useQueryClient() - const [dismissedDids, setDismissedDids] = useState>(new Set()) + const onDismiss = useCallback( + (dismissedDid: string) => { + queryClient.setQueryData( + suggestedFollowsByActorQueryKey(did), + (previous: typeof data) => { + if (!previous) return previous + return { + ...previous, + suggestions: previous.suggestions.filter( + s => s.did !== dismissedDid, + ), + } + }, + ) + }, + [did, queryClient], + ) - const onDismiss = useCallback((dismissedDid: string) => { - setDismissedDids(prev => new Set(prev).add(dismissedDid)) - }, []) - - // Combine profiles from the actor-specific query with fallback suggestions - const allProfiles = useMemo(() => { - const actorProfiles = data?.suggestions ?? [] - const fallbackProfiles = - moreSuggestions?.pages.flatMap(page => - page.actors.map(actor => ({actor, recId: page.recId})), - ) ?? [] - - // Dedupe by did, preferring actor-specific profiles - const seen = new Set() - const combined: {actor: bsky.profile.AnyProfileView; recId?: number}[] = [] - - for (const profile of actorProfiles) { - if (!seen.has(profile.did)) { - seen.add(profile.did) - combined.push({actor: profile, recId: data?.recId}) - } - } - - for (const profile of fallbackProfiles) { - if (!seen.has(profile.actor.did) && profile.actor.did !== did) { - seen.add(profile.actor.did) - combined.push(profile) - } - } - - return combined - }, [data?.suggestions, moreSuggestions?.pages, did, data?.recId]) - - const filteredProfiles = useMemo(() => { - return allProfiles.filter(p => !dismissedDids.has(p.actor.did)) - }, [allProfiles, dismissedDids]) - - // Fetch more when running low - useEffect(() => { - if ( - moderationOpts && - filteredProfiles.length < maxLength && - hasNextPage && - !isFetchingNextPage - ) { - void fetchNextPage() - } - }, [ - filteredProfiles.length, - maxLength, - hasNextPage, - isFetchingNextPage, - fetchNextPage, - moderationOpts, - ]) + const profiles = useMemo(() => { + return (data?.suggestions ?? []).map(profile => ({ + actor: profile, + recId: data?.recId, + })) + }, [data?.suggestions, data?.recId]) return ( >(new Set()) @@ -326,66 +277,29 @@ export function SuggestedFollowsHome() { setDismissedDids(prev => new Set(prev).add(did)) }, []) - // Combine profiles from experimental query with paginated suggestions const allProfiles = useMemo(() => { - const fallbackProfiles = - moreSuggestions?.pages.flatMap(page => - page.actors.map(actor => ({actor, recId: page.recId})), - ) ?? [] - - // Dedupe by did, preferring experimental profiles - const seen = new Set() - const combined: Array<{ + const result: Array<{ actor: bsky.profile.AnyProfileView - recId?: number + recId?: string }> = [] for (const profile of experimentalProfiles) { - if (!seen.has(profile.did)) { - seen.add(profile.did) - combined.push({actor: profile, recId: undefined}) - } + result.push({actor: profile, recId: undefined}) } - for (const profile of fallbackProfiles) { - if (!seen.has(profile.actor.did)) { - seen.add(profile.actor.did) - combined.push(profile) - } - } - - return combined - }, [experimentalProfiles, moreSuggestions?.pages]) + return result + }, [experimentalProfiles]) const filteredProfiles = useMemo(() => { return allProfiles.filter(p => !dismissedDids.has(p.actor.did)) }, [allProfiles, dismissedDids]) - // Fetch more when running low - useEffect(() => { - if ( - moderationOpts && - filteredProfiles.length < maxLength && - hasNextPage && - !isFetchingNextPage - ) { - void fetchNextPage() - } - }, [ - filteredProfiles.length, - maxLength, - hasNextPage, - isFetchingNextPage, - fetchNextPage, - moderationOpts, - ]) - return ( @@ -400,14 +314,16 @@ export function ProfileGrid({ viewContext = 'feed', onDismiss, isVisible = true, + onRequestHide, }: { isSuggestionsLoading: boolean - profiles: {actor: bsky.profile.AnyProfileView; recId?: number}[] + profiles: {actor: bsky.profile.AnyProfileView; recId?: string}[] totalProfileCount?: number error: Error | null viewContext: 'profile' | 'profileHeader' | 'feed' onDismiss?: (did: string) => void isVisible?: boolean + onRequestHide?: () => void }) { const t = useTheme() const ax = useAnalytics() @@ -651,6 +567,13 @@ export function ProfileGrid({ // Use totalProfileCount (before dismissals) for minLength check on initial render. const profileCountForMinCheck = totalProfileCount ?? profiles.length + + useEffect(() => { + if (error || (!isLoading && profileCountForMinCheck < minLength)) { + onRequestHide?.() + } + }, [error, isLoading, onRequestHide, profileCountForMinCheck, minLength]) + if (error || (!isLoading && profileCountForMinCheck < minLength)) { ax.logger.debug(`Not enough profiles to show suggested follows`) return null diff --git a/src/screens/Profile/Header/ProfileHeaderStandard.tsx b/src/screens/Profile/Header/ProfileHeaderStandard.tsx index d2bfa071e4..8e0055cc8c 100644 --- a/src/screens/Profile/Header/ProfileHeaderStandard.tsx +++ b/src/screens/Profile/Header/ProfileHeaderStandard.tsx @@ -75,6 +75,8 @@ let ProfileHeaderStandard = ({ const [, queueUnblock] = useProfileBlockMutationQueue(profile) const unblockPromptControl = Prompt.usePromptControl() const [showSuggestedFollows, setShowSuggestedFollows] = useState(false) + const [hasSeenAllSuggestedFollows, setHasSeenAllSuggestedFollows] = + useState(false) const isBlockedUser = profile.viewer?.blocking || profile.viewer?.blockedBy || @@ -84,7 +86,8 @@ let ProfileHeaderStandard = ({ try { await queueUnblock() Toast.show(_(msg({message: 'Account unblocked', context: 'toast'}))) - } catch (e: any) { + } catch (err) { + const e = err as Error if (e?.name !== 'AbortError') { logger.error('Failed to unblock account', {message: e}) Toast.show(_(msg`There was an issue! ${e.toString()}`), {type: 'error'}) @@ -92,6 +95,11 @@ let ProfileHeaderStandard = ({ } } + const onRequestHide = () => { + setHasSeenAllSuggestedFollows(true) + setShowSuggestedFollows(false) + } + const isMe = currentAccount?.did === profile.did const {isActive: live} = useActorStatus(profile) @@ -192,7 +200,9 @@ let ProfileHeaderStandard = ({ description={_( msg`The account will be able to interact with you after unblocking.`, )} - onConfirm={unblockAccount} + onConfirm={() => { + void unblockAccount() + }} confirmButtonCta={ profile.viewer?.blocking ? _(msg`Unblock`) : _(msg`Block`) } @@ -201,8 +211,9 @@ let ProfileHeaderStandard = ({ ) @@ -254,7 +265,8 @@ export function HeaderStandardButtons({ )}`, ), ) - } catch (e: any) { + } catch (err) { + const e = err as Error if (e?.name !== 'AbortError') { logger.error('Failed to follow', {message: String(e)}) Toast.show(_(msg`There was an issue! ${e.toString()}`), { @@ -280,7 +292,8 @@ export function HeaderStandardButtons({ ), {type: 'default'}, ) - } catch (e: any) { + } catch (err) { + const e = err as Error if (e?.name !== 'AbortError') { logger.error('Failed to unfollow', {message: String(e)}) Toast.show(_(msg`There was an issue! ${e.toString()}`), { @@ -295,7 +308,8 @@ export function HeaderStandardButtons({ try { await queueUnblock() Toast.show(_(msg({message: 'Account unblocked', context: 'toast'}))) - } catch (e: any) { + } catch (err) { + const e = err as Error if (e?.name !== 'AbortError') { logger.error('Failed to unblock account', {message: e}) Toast.show(_(msg`There was an issue! ${e.toString()}`), {type: 'error'}) @@ -400,7 +414,9 @@ export function HeaderStandardButtons({ description={_( msg`The account will be able to interact with you after unblocking.`, )} - onConfirm={unblockAccount} + onConfirm={() => { + void unblockAccount() + }} confirmButtonCta={_(msg`Unblock`)} confirmButtonColor="negative" /> diff --git a/src/screens/Profile/Header/SuggestedFollows.tsx b/src/screens/Profile/Header/SuggestedFollows.tsx index f8bec7d142..79094d03d6 100644 --- a/src/screens/Profile/Header/SuggestedFollows.tsx +++ b/src/screens/Profile/Header/SuggestedFollows.tsx @@ -1,12 +1,11 @@ -import {useCallback, useEffect, useMemo, useState} from 'react' +import {useCallback, useMemo} from 'react' +import {useQueryClient} from '@tanstack/react-query' import {AccordionAnimation} from '#/lib/custom-animations/AccordionAnimation' -import {useModerationOpts} from '#/state/preferences/moderation-opts' import { + suggestedFollowsByActorQueryKey, useSuggestedFollowsByActorQuery, - useSuggestedFollowsQuery, } from '#/state/queries/suggested-follows' -import {useBreakpoints} from '#/alf' import {ProfileGrid} from '#/components/FeedInterstitials' import {IS_ANDROID} from '#/env' import type * as bsky from '#/types/bsky' @@ -14,15 +13,15 @@ import type * as bsky from '#/types/bsky' export function ProfileHeaderSuggestedFollows({ isExpanded, actorDid, + onRequestHide, }: { isExpanded: boolean actorDid: string + onRequestHide: () => void }) { - const {allProfiles, filteredProfiles, onDismiss, isLoading, error} = + const {profiles, onDismiss, isLoading, error} = useProfileHeaderSuggestions(actorDid) - if (!allProfiles.length && !isLoading) return null - /* NOTE (caidanw): * Android does not work well with this feature yet. * This issue stems from Android not allowing dragging on clickable elements in the profile header. @@ -34,92 +33,51 @@ export function ProfileHeaderSuggestedFollows({ ) } function useProfileHeaderSuggestions(actorDid: string) { - const {gtMobile} = useBreakpoints() - const moderationOpts = useModerationOpts() - const maxLength = gtMobile ? 4 : 12 const {isLoading, data, error} = useSuggestedFollowsByActorQuery({ did: actorDid, }) - const { - data: moreSuggestions, - fetchNextPage, - hasNextPage, - isFetchingNextPage, - } = useSuggestedFollowsQuery({limit: 25}) + const queryClient = useQueryClient() - const [dismissedDids, setDismissedDids] = useState>(new Set()) + const onDismiss = useCallback( + (dismissedDid: string) => { + queryClient.setQueryData( + suggestedFollowsByActorQueryKey(actorDid), + (previous: typeof data) => { + if (!previous) return previous + return { + ...previous, + suggestions: previous.suggestions.filter( + s => s.did !== dismissedDid, + ), + } + }, + ) + }, + [actorDid, queryClient], + ) - const onDismiss = useCallback((did: string) => { - setDismissedDids(prev => new Set(prev).add(did)) - }, []) - - // Combine profiles from the actor-specific query with fallback suggestions - const allProfiles = useMemo(() => { - const actorProfiles = data?.suggestions ?? [] - const fallbackProfiles = - moreSuggestions?.pages.flatMap(page => - page.actors.map(actor => ({actor, recId: page.recId})), - ) ?? [] - - // Dedupe by did, preferring actor-specific profiles - const seen = new Set() - const combined: {actor: bsky.profile.AnyProfileView; recId?: number}[] = [] - - for (const profile of actorProfiles) { - if (!seen.has(profile.did)) { - seen.add(profile.did) - combined.push({actor: profile, recId: data?.recId}) - } - } - - for (const profile of fallbackProfiles) { - if (!seen.has(profile.actor.did) && profile.actor.did !== actorDid) { - seen.add(profile.actor.did) - combined.push(profile) - } - } - - return combined - }, [data?.suggestions, moreSuggestions?.pages, actorDid, data?.recId]) - - const filteredProfiles = useMemo(() => { - return allProfiles.filter(p => !dismissedDids.has(p.actor.did)) - }, [allProfiles, dismissedDids]) - - // Fetch more when running low - useEffect(() => { - if ( - moderationOpts && - filteredProfiles.length < maxLength && - hasNextPage && - !isFetchingNextPage - ) { - void fetchNextPage() - } - }, [ - filteredProfiles.length, - maxLength, - hasNextPage, - isFetchingNextPage, - fetchNextPage, - moderationOpts, - ]) + const profiles = useMemo(() => { + return (data?.suggestions ?? []).map(profile => ({ + actor: profile as bsky.profile.AnyProfileView, + recId: data?.recId, + })) + }, [data?.suggestions, data?.recId]) return { - allProfiles, - filteredProfiles, + profiles, onDismiss, isLoading, error, diff --git a/src/state/queries/profile.ts b/src/state/queries/profile.ts index d5b8151928..a42807405a 100644 --- a/src/state/queries/profile.ts +++ b/src/state/queries/profile.ts @@ -5,8 +5,8 @@ import { type AppBskyActorGetProfiles, type AppBskyActorProfile, type AppBskyGraphGetFollows, + type AtpAgent, AtUri, - type BskyAgent, type ComAtprotoRepoUploadBlob, type Un$Typed, } from '@atproto/api' @@ -203,19 +203,19 @@ export function useProfileUpdateMutation() { (res => { if (typeof newUserAvatar !== 'undefined') { if (newUserAvatar === null && res.data.avatar) { - // url hasnt cleared yet + // url hasn't cleared yet return false } else if (res.data.avatar === profile.avatar) { - // url hasnt changed yet + // url hasn't changed yet return false } } if (typeof newUserBanner !== 'undefined') { if (newUserBanner === null && res.data.banner) { - // url hasnt cleared yet + // url hasn't cleared yet return false } else if (res.data.banner === profile.banner) { - // url hasnt changed yet + // url hasn't changed yet return false } } @@ -231,10 +231,10 @@ export function useProfileUpdateMutation() { }, async onSuccess(_, variables) { // invalidate cache - queryClient.invalidateQueries({ + void queryClient.invalidateQueries({ queryKey: RQKEY(variables.profile.did), }) - queryClient.invalidateQueries({ + void queryClient.invalidateQueries({ queryKey: [profilesQueryKeyRoot, [variables.profile.did]], }) await updateProfileVerificationCache({profile: variables.profile}) @@ -329,7 +329,7 @@ export function useProfileFollowMutationQueue( } if (finalFollowingUri) { - agent.app.bsky.graph + void agent.app.bsky.graph .getSuggestedFollowsByActor({ actor: did, }) @@ -474,7 +474,7 @@ function useProfileMuteMutation() { await agent.mute(did) }, onSuccess() { - queryClient.invalidateQueries({queryKey: RQKEY_MY_MUTED()}) + void queryClient.invalidateQueries({queryKey: RQKEY_MY_MUTED()}) }, }) } @@ -487,7 +487,7 @@ function useProfileUnmuteMutation() { await agent.unmute(did) }, onSuccess() { - queryClient.invalidateQueries({queryKey: RQKEY_MY_MUTED()}) + void queryClient.invalidateQueries({queryKey: RQKEY_MY_MUTED()}) }, }) } @@ -527,7 +527,7 @@ export function useProfileBlockMutationQueue( updateProfileShadow(queryClient, did, { blockingUri: finalBlockingUri, }) - queryClient.invalidateQueries({queryKey: [RQKEY_LIST_CONVOS]}) + void queryClient.invalidateQueries({queryKey: [RQKEY_LIST_CONVOS]}) }, }) @@ -565,7 +565,7 @@ function useProfileBlockMutation() { ) }, onSuccess(_, {did}) { - queryClient.invalidateQueries({queryKey: RQKEY_MY_BLOCKED()}) + void queryClient.invalidateQueries({queryKey: RQKEY_MY_BLOCKED()}) resetProfilePostsQueries(queryClient, did, 1000) }, }) @@ -593,7 +593,7 @@ function useProfileUnblockMutation() { } async function whenAppViewReady( - agent: BskyAgent, + agent: AtpAgent, actor: string, fn: (res: AppBskyActorGetProfile.Response) => boolean, ) { diff --git a/src/state/queries/suggested-follows.ts b/src/state/queries/suggested-follows.ts index c7a6e5f752..8dd4e3b6c7 100644 --- a/src/state/queries/suggested-follows.ts +++ b/src/state/queries/suggested-follows.ts @@ -2,107 +2,24 @@ import { type AppBskyActorDefs, type AppBskyActorGetSuggestions, type AppBskyGraphGetSuggestedFollowsByActor, - moderateProfile, } from '@atproto/api' import { type InfiniteData, type QueryClient, - type QueryKey, - useInfiniteQuery, useQuery, } from '@tanstack/react-query' -import { - aggregateUserInterests, - createBskyTopicsHeader, -} from '#/lib/api/feed/utils' -import {getContentLanguages} from '#/state/preferences/languages' import {STALE} from '#/state/queries' -import {usePreferencesQuery} from '#/state/queries/preferences' -import {useAgent, useSession} from '#/state/session' -import {useModerationOpts} from '../preferences/moderation-opts' +import {useAgent} from '#/state/session' const suggestedFollowsQueryKeyRoot = 'suggested-follows' -const suggestedFollowsQueryKey = (options?: SuggestedFollowsOptions) => [ - suggestedFollowsQueryKeyRoot, - options, -] const suggestedFollowsByActorQueryKeyRoot = 'suggested-follows-by-actor' -const suggestedFollowsByActorQueryKey = (did: string) => [ +export const suggestedFollowsByActorQueryKey = (did: string) => [ suggestedFollowsByActorQueryKeyRoot, did, ] -type SuggestedFollowsOptions = {limit?: number; subsequentPageLimit?: number} - -export function useSuggestedFollowsQuery(options?: SuggestedFollowsOptions) { - const {currentAccount} = useSession() - const agent = useAgent() - const moderationOpts = useModerationOpts() - const {data: preferences} = usePreferencesQuery() - const limit = options?.limit || 25 - - return useInfiniteQuery< - AppBskyActorGetSuggestions.OutputSchema, - Error, - InfiniteData, - QueryKey, - string | undefined - >({ - enabled: !!moderationOpts && !!preferences, - staleTime: STALE.HOURS.ONE, - queryKey: suggestedFollowsQueryKey(options), - queryFn: async ({pageParam}) => { - const contentLangs = getContentLanguages().join(',') - const maybeDifferentLimit = - options?.subsequentPageLimit && pageParam - ? options.subsequentPageLimit - : limit - const res = await agent.app.bsky.actor.getSuggestions( - { - limit: maybeDifferentLimit, - cursor: pageParam, - }, - { - headers: { - ...createBskyTopicsHeader(aggregateUserInterests(preferences)), - 'Accept-Language': contentLangs, - }, - }, - ) - - res.data.actors = res.data.actors - .filter( - actor => - !moderateProfile(actor, moderationOpts!).ui('profileList').filter, - ) - .filter(actor => { - const viewer = actor.viewer - if (viewer) { - if ( - viewer.following || - viewer.muted || - viewer.mutedByList || - viewer.blockedBy || - viewer.blocking - ) { - return false - } - } - if (actor.did === currentAccount?.did) { - return false - } - return true - }) - - return res.data - }, - initialPageParam: undefined, - getNextPageParam: lastPage => lastPage.cursor, - }) -} - export function useSuggestedFollowsByActorQuery({ did, enabled, @@ -120,10 +37,10 @@ export function useSuggestedFollowsByActorQuery({ const res = await agent.app.bsky.graph.getSuggestedFollowsByActor({ actor: did, }) - const suggestions = res.data.isFallback - ? [] - : res.data.suggestions.filter(profile => !profile.viewer?.following) - return {suggestions, recId: res.data.recId} + const suggestions = res.data.suggestions.filter( + profile => !profile.viewer?.following, + ) + return {suggestions, recId: res.data.recIdStr} }, enabled, }) diff --git a/src/view/com/posts/PostFeed.tsx b/src/view/com/posts/PostFeed.tsx index 903d68f04b..f813dc9358 100644 --- a/src/view/com/posts/PostFeed.tsx +++ b/src/view/com/posts/PostFeed.tsx @@ -15,8 +15,7 @@ import { AppBskyEmbedVideo, type AppBskyFeedDefs, } from '@atproto/api' -import {msg} from '@lingui/core/macro' -import {useLingui} from '@lingui/react' +import {useLingui} from '@lingui/react/macro' import {useQueryClient} from '@tanstack/react-query' import {DISCOVER_FEED_URI, KNOWN_SHUTDOWN_FEEDS} from '#/lib/constants' @@ -226,14 +225,16 @@ let PostFeed = ({ savedFeedConfig?: AppBskyActorDefs.SavedFeed initialNumToRender?: number isVideoFeed?: boolean + lastFetchDate?: () => number }): React.ReactNode => { const ax = useAnalytics() - const {_} = useLingui() + const {t: l} = useLingui() const queryClient = useQueryClient() const {currentAccount, hasSession} = useSession() const initialNumToRender = useInitialNumToRender() const feedFeedback = useFeedFeedbackContext() const [isPTRing, setIsPTRing] = useState(false) + // eslint-disable-next-line react-hooks/purity const lastFetchRef = useRef(Date.now()) const [feedType, feedUriOrActorDid, feedTab] = feed.split('|') const {gtMobile} = useBreakpoints() @@ -271,14 +272,17 @@ let PostFeed = ({ fetchNextPage, } = usePostFeedQuery(feed, feedParams, opts) const lastFetchedAt = data?.pages[0].fetchedAt - if (lastFetchedAt) { - lastFetchRef.current = lastFetchedAt - } const isEmpty = useMemo( () => !isFetching && !data?.pages?.some(page => page.slices.length), [isFetching, data], ) + useEffect(() => { + if (lastFetchedAt) { + lastFetchRef.current = lastFetchedAt + } + }, [lastFetchedAt]) + const checkForNew = useNonReactiveCallback(async () => { if (!data?.pages[0] || isFetching || !onHasNew || !enabled || disablePoll) { return @@ -292,7 +296,7 @@ let PostFeed = ({ try { if (await pollLatest(data.pages[0])) { if (isEmpty) { - refetch() + void refetch() } else { onHasNew(true) } @@ -333,7 +337,7 @@ let PostFeed = ({ const timeSinceFirstLoad = Date.now() - lastFetchRef.current if (isEmpty || timeSinceFirstLoad > CHECK_LATEST_AFTER) { // check for new on enable (aka on focus) - checkForNew() + void checkForNew() } } }, [enabled, isEmpty, disablePoll, checkForNew]) @@ -343,14 +347,14 @@ let PostFeed = ({ const subscription = AppState.addEventListener('change', nextAppState => { // check for new on app foreground if (nextAppState === 'active') { - checkForNew() + void checkForNew() } }) cleanup1 = () => subscription.remove() if (pollInterval) { // check for new on interval const i = setInterval(() => { - checkForNew() + void checkForNew() }, pollInterval) cleanup2 = () => clearInterval(i) } @@ -363,7 +367,7 @@ let PostFeed = ({ const followProgressGuide = useProgressGuide('follow-10') const followAndLikeProgressGuide = useProgressGuide('like-10-and-follow-7') - const showProgressIntersitial = + const showProgressInterstitial = (followProgressGuide || followAndLikeProgressGuide) && !rightNavVisible const {trendingVideoDisabled} = useTrendingSettings() @@ -494,7 +498,7 @@ let PostFeed = ({ if (hasSession) { if (feedKind === 'discover') { if (sliceIndex === 0) { - if (showProgressIntersitial) { + if (showProgressInterstitial) { arr.push({ type: 'interstitialProgressGuide', key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt, @@ -672,7 +676,7 @@ let PostFeed = ({ feedUriOrActorDid, feedTab, hasSession, - showProgressIntersitial, + showProgressInterstitial, trendingVideoDisabled, gtMobile, isVideoFeed, @@ -683,16 +687,12 @@ let PostFeed = ({ blockedOrMutedAuthors, ]) - useEffect(() => { - if (enabled === false) { - setIsPTRing(false) - } - }, [enabled]) - // events // = const onRefresh = useCallback(async () => { + if (!enabled) return + ax.metric('feed:refresh', { feedType: feedType, feedUrl: feed, @@ -706,7 +706,7 @@ let PostFeed = ({ logger.error('Failed to refresh posts feed', {message: err}) } setIsPTRing(false) - }, [ax, refetch, setIsPTRing, onHasNew, feed, feedType]) + }, [ax, refetch, setIsPTRing, onHasNew, feed, feedType, enabled]) const onEndReached = useCallback(async () => { if (isFetching || !hasNextPage || isError) return @@ -733,12 +733,12 @@ let PostFeed = ({ ]) const onPressTryAgain = useCallback(() => { - refetch() + void refetch() onHasNew?.(false) }, [refetch, onHasNew]) const onPressRetryLoadMore = useCallback(() => { - fetchNextPage() + void fetchNextPage() }, [fetchNextPage]) // rendering @@ -760,9 +760,7 @@ let PostFeed = ({ } else if (row.type === 'loadMoreError') { return ( ) @@ -861,7 +859,7 @@ let PostFeed = ({ error, onPressTryAgain, savedFeedConfig, - _, + l, onPressRetryLoadMore, feedType, feedUriOrActorDid, @@ -997,19 +995,19 @@ let PostFeed = ({ testID={testID ? `${testID}-flatlist` : undefined} ref={scrollElRef} data={feedItems} - keyExtractor={item => item.key} + keyExtractor={(item: FeedRow) => item.key} renderItem={renderItem} ListFooterComponent={FeedFooter} ListHeaderComponent={ListHeaderComponent} refreshing={isPTRing} - onRefresh={onRefresh} + onRefresh={() => void onRefresh()} headerOffset={headerOffset} progressViewOffset={progressViewOffset} contentContainerStyle={{ minHeight: Dimensions.get('window').height * 1.5, }} onScrolledDownChange={handleScrolledDownChange} - onEndReached={onEndReached} + onEndReached={() => void onEndReached()} onEndReachedThreshold={2} // number of posts left to trigger load more removeClippedSubviews={true} extraData={extraData}