From c71db3172cc2407b636788977df9fc091d5af3b9 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Mon, 26 Jan 2026 22:41:10 +0200 Subject: [PATCH] add recId to feed interstitials, fix animation on native --- src/components/FeedInterstitials.tsx | 220 ++++++++++++--------------- 1 file changed, 101 insertions(+), 119 deletions(-) diff --git a/src/components/FeedInterstitials.tsx b/src/components/FeedInterstitials.tsx index 6e14f0bab5..26a65ccf5f 100644 --- a/src/components/FeedInterstitials.tsx +++ b/src/components/FeedInterstitials.tsx @@ -1,6 +1,12 @@ -import React, {useCallback, useEffect, useRef} from 'react' +import {useCallback, useEffect, useMemo, useRef, useState} from 'react' import {ScrollView, View} from 'react-native' -import Animated, {LinearTransition} from 'react-native-reanimated' +import Animated, { + Easing, + FadeIn, + FadeOut, + LayoutAnimationConfig, + LinearTransition, +} from 'react-native-reanimated' import {type AppBskyFeedDefs, AtUri} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -21,6 +27,7 @@ import {type SeenPost} from '#/state/userActionHistory' import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture' import { atoms as a, + native, useBreakpoints, useTheme, type ViewStyleProp, @@ -152,7 +159,7 @@ function sortSeenPosts(postA: SeenPost, postB: SeenPost): 0 | 1 | -1 { function useExperimentalSuggestedUsersQuery() { const {currentAccount} = useSession() const userActionSnapshot = userActionHistory.useActionHistorySnapshot() - const dids = React.useMemo(() => { + const dids = useMemo(() => { const {likes, follows, followSuggestions, seen} = userActionSnapshot const likeDids = likes .map(l => new AtUri(l)) @@ -225,67 +232,54 @@ export function SuggestedFollowsProfile({did}: {did: string}) { isFetchingNextPage, } = useSuggestedFollowsQuery({limit: 25}) - const [dismissedDids, setDismissedDids] = React.useState>( - new Set(), - ) - const [dismissingDids, setDismissingDids] = React.useState>( - new Set(), - ) + const [dismissedDids, setDismissedDids] = useState>(new Set()) - const onDismiss = React.useCallback((dismissedDid: string) => { - // Start the fade animation - setDismissingDids(prev => new Set(prev).add(dismissedDid)) - // After animation completes, actually remove from list - setTimeout(() => { - setDismissedDids(prev => new Set(prev).add(dismissedDid)) - setDismissingDids(prev => { - const next = new Set(prev) - next.delete(dismissedDid) - return next - }) - }, DISMISS_ANIMATION_DURATION) + const onDismiss = useCallback((dismissedDid: string) => { + setDismissedDids(prev => new Set(prev).add(dismissedDid)) }, []) // Combine profiles from the actor-specific query with fallback suggestions - const allProfiles = React.useMemo(() => { + const allProfiles = useMemo(() => { const actorProfiles = data?.suggestions ?? [] const fallbackProfiles = - moreSuggestions?.pages.flatMap(page => page.actors) ?? [] + 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: bsky.profile.AnyProfileView[] = [] + const combined: {actor: bsky.profile.AnyProfileView; recId?: number}[] = [] for (const profile of actorProfiles) { if (!seen.has(profile.did)) { seen.add(profile.did) - combined.push(profile) + combined.push({actor: profile, recId: data?.recId}) } } for (const profile of fallbackProfiles) { - if (!seen.has(profile.did) && profile.did !== did) { - seen.add(profile.did) + 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?.suggestions, moreSuggestions?.pages, did, data?.recId]) - const filteredProfiles = React.useMemo(() => { - return allProfiles.filter(p => !dismissedDids.has(p.did)) + const filteredProfiles = useMemo(() => { + return allProfiles.filter(p => !dismissedDids.has(p.actor.did)) }, [allProfiles, dismissedDids]) // Fetch more when running low - React.useEffect(() => { + useEffect(() => { if ( moderationOpts && filteredProfiles.length < maxLength && hasNextPage && !isFetchingNextPage ) { - fetchNextPage() + void fetchNextPage() } }, [ filteredProfiles.length, @@ -301,11 +295,9 @@ export function SuggestedFollowsProfile({did}: {did: string}) { isSuggestionsLoading={isSuggestionsLoading} profiles={filteredProfiles} totalProfileCount={allProfiles.length} - recId={data?.recId} error={error} viewContext="profile" onDismiss={onDismiss} - dismissingDids={dismissingDids} /> ) } @@ -327,46 +319,36 @@ export function SuggestedFollowsHome() { error: suggestionsError, } = useSuggestedFollowsQuery({limit: 25}) - const [dismissedDids, setDismissedDids] = React.useState>( - new Set(), - ) - const [dismissingDids, setDismissingDids] = React.useState>( - new Set(), - ) + const [dismissedDids, setDismissedDids] = useState>(new Set()) - const onDismiss = React.useCallback((did: string) => { - // Start the fade animation - setDismissingDids(prev => new Set(prev).add(did)) - // After animation completes, actually remove from list - setTimeout(() => { - setDismissedDids(prev => new Set(prev).add(did)) - setDismissingDids(prev => { - const next = new Set(prev) - next.delete(did) - return next - }) - }, DISMISS_ANIMATION_DURATION) + const onDismiss = useCallback((did: string) => { + setDismissedDids(prev => new Set(prev).add(did)) }, []) // Combine profiles from experimental query with paginated suggestions - const allProfiles = React.useMemo(() => { + const allProfiles = useMemo(() => { const fallbackProfiles = - moreSuggestions?.pages.flatMap(page => page.actors) ?? [] + moreSuggestions?.pages.flatMap(page => + page.actors.map(actor => ({actor, recId: page.recId})), + ) ?? [] // Dedupe by did, preferring experimental profiles const seen = new Set() - const combined: bsky.profile.AnyProfileView[] = [] + const combined: Array<{ + actor: bsky.profile.AnyProfileView + recId?: number + }> = [] for (const profile of experimentalProfiles) { if (!seen.has(profile.did)) { seen.add(profile.did) - combined.push(profile) + combined.push({actor: profile, recId: undefined}) } } for (const profile of fallbackProfiles) { - if (!seen.has(profile.did)) { - seen.add(profile.did) + if (!seen.has(profile.actor.did)) { + seen.add(profile.actor.did) combined.push(profile) } } @@ -374,19 +356,19 @@ export function SuggestedFollowsHome() { return combined }, [experimentalProfiles, moreSuggestions?.pages]) - const filteredProfiles = React.useMemo(() => { - return allProfiles.filter(p => !dismissedDids.has(p.did)) + const filteredProfiles = useMemo(() => { + return allProfiles.filter(p => !dismissedDids.has(p.actor.did)) }, [allProfiles, dismissedDids]) // Fetch more when running low - React.useEffect(() => { + useEffect(() => { if ( moderationOpts && filteredProfiles.length < maxLength && hasNextPage && !isFetchingNextPage ) { - fetchNextPage() + void fetchNextPage() } }, [ filteredProfiles.length, @@ -405,7 +387,6 @@ export function SuggestedFollowsHome() { error={experimentalError || suggestionsError} viewContext="feed" onDismiss={onDismiss} - dismissingDids={dismissingDids} /> ) } @@ -415,16 +396,13 @@ export function ProfileGrid({ error, profiles, totalProfileCount, - recId, viewContext = 'feed', onDismiss, - dismissingDids, isVisible = true, }: { isSuggestionsLoading: boolean - profiles: bsky.profile.AnyProfileView[] + profiles: {actor: bsky.profile.AnyProfileView; recId?: number}[] totalProfileCount?: number - recId?: number error: Error | null dismissingDids?: Set viewContext: 'profile' | 'profileHeader' | 'feed' @@ -463,18 +441,18 @@ export function ProfileGrid({ const profilesToShow = profiles.slice(0, maxLength) profilesToShow.forEach((profile, index) => { - if (!seenProfilesRef.current.has(profile.did)) { - seenProfilesRef.current.add(profile.did) + if (!seenProfilesRef.current.has(profile.actor.did)) { + seenProfilesRef.current.add(profile.actor.did) ax.metric('suggestedUser:seen', { logContext, - recId, + recId: profile.recId, position: index, - suggestedDid: profile.did, + suggestedDid: profile.actor.did, category: null, }) } }) - }, [ax, isLoading, error, profiles, maxLength, logContext, recId]) + }, [ax, isLoading, error, profiles, maxLength, logContext]) // For profile header, fire when isVisible becomes true useEffect(() => { @@ -540,8 +518,15 @@ export function ProfileGrid({ ? null : profiles.slice(0, maxLength).map((profile, index) => ( { ax.metric('suggestedUser:press', { logContext: isFeedContext ? 'InterstitialDiscover' : 'InterstitialProfile', - recId, + recId: profile.recId, position: index, - suggestedDid: profile.did, + suggestedDid: profile.actor.did, category: null, }) }} @@ -581,14 +561,14 @@ export function ProfileGrid({ label={_(msg`Dismiss this suggestion`)} onPress={e => { e.preventDefault() - onDismiss(profile.did) + onDismiss(profile.actor.did) ax.metric('suggestedUser:dismiss', { logContext: isFeedContext ? 'InterstitialDiscover' : 'InterstitialProfile', position: index, - suggestedDid: profile.did, - recId, + suggestedDid: profile.actor.did, + recId: profile.recId, }) }} style={[ @@ -621,18 +601,18 @@ export function ProfileGrid({ a.mb_auto, ]}> - {gtMobile ? ( - - - {content} + + {gtMobile ? ( + + + {content} + - - ) : ( - - - {content} + ) : ( + + + {content} - {!isProfileHeaderContext && ( - { - followDialogControl.open() - ax.metric('suggestedUser:seeMore', { - logContext: 'Explore', - }) - }} - /> - )} - - - )} + {!isProfileHeaderContext && ( + { + followDialogControl.open() + ax.metric('suggestedUser:seeMore', { + logContext: 'Explore', + }) + }} + /> + )} + + + )} + ) } @@ -795,7 +777,7 @@ export function SuggestedFeeds() { const navigation = useNavigation() const {gtMobile} = useBreakpoints() - const feeds = React.useMemo(() => { + const feeds = useMemo(() => { const items: AppBskyFeedDefs.GeneratorView[] = [] if (!data) return items