diff --git a/src/components/FeedInterstitials.tsx b/src/components/FeedInterstitials.tsx index 1399639b08..add8ffecd4 100644 --- a/src/components/FeedInterstitials.tsx +++ b/src/components/FeedInterstitials.tsx @@ -288,6 +288,8 @@ export function ProfileGrid({ logContext, recId, position: index, + suggestedDid: profile.did, + category: null, }, {statsig: true}, ) @@ -368,6 +370,8 @@ export function ProfileGrid({ : 'InterstitialProfile', recId, position: index, + suggestedDid: profile.did, + category: null, }) }} style={[ @@ -428,6 +432,8 @@ export function ProfileGrid({ location: 'Card', recId, position: index, + suggestedDid: profile.did, + category: null, }) }} /> diff --git a/src/components/ProgressGuide/FollowDialog.tsx b/src/components/ProgressGuide/FollowDialog.tsx index 1a8eb59165..74248a6f13 100644 --- a/src/components/ProgressGuide/FollowDialog.tsx +++ b/src/components/ProgressGuide/FollowDialog.tsx @@ -236,6 +236,8 @@ function DialogInner({guide}: {guide: Follow10ProgressGuide}) { const seenProfilesRef = useRef>(new Set()) const itemsRef = useRef(items) itemsRef.current = items + const selectedInterestRef = useRef(selectedInterest) + selectedInterestRef.current = selectedInterest const onViewableItemsChanged = useRef( ({viewableItems}: {viewableItems: ViewToken[]}) => { @@ -253,6 +255,8 @@ function DialogInner({guide}: {guide: Follow10ProgressGuide}) { logContext: 'ProgressGuide', recId: undefined, position: position !== -1 ? position : 0, + suggestedDid: item.profile.did, + category: selectedInterestRef.current, }, {statsig: true}, ) diff --git a/src/logger/metrics.ts b/src/logger/metrics.ts index 4ad718382f..6acf177ac4 100644 --- a/src/logger/metrics.ts +++ b/src/logger/metrics.ts @@ -311,6 +311,8 @@ export type MetricEvents = { location: 'Card' | 'Profile' recId?: number position: number + suggestedDid: string + category: string | null } 'suggestedUser:press': { logContext: @@ -320,6 +322,8 @@ export type MetricEvents = { | 'Onboarding' recId?: number position: number + suggestedDid: string + category: string | null } 'suggestedUser:seen': { logContext: @@ -331,6 +335,8 @@ export type MetricEvents = { | 'ProgressGuide' recId?: number position: number + suggestedDid: string + category: string | null } 'suggestedUser:seeMore': { logContext: diff --git a/src/screens/Onboarding/StepSuggestedAccounts/index.tsx b/src/screens/Onboarding/StepSuggestedAccounts/index.tsx index fd7ca29a3f..5c10f8fc2b 100644 --- a/src/screens/Onboarding/StepSuggestedAccounts/index.tsx +++ b/src/screens/Onboarding/StepSuggestedAccounts/index.tsx @@ -1,4 +1,11 @@ -import {useContext, useEffect, useMemo, useRef, useState} from 'react' +import { + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from 'react' import {View} from 'react-native' import {type ModerationOpts} from '@atproto/api' import {msg, Trans} from '@lingui/macro' @@ -123,26 +130,27 @@ export function StepSuggestedAccounts() { const canFollowAll = followableDids.length > 0 && !isFollowingAll - // Track seen profiles + // Track seen profiles - shared ref across all cards const seenProfilesRef = useRef>(new Set()) - useEffect(() => { - if (isLoading || !moderationOpts || !suggestedUsers?.actors.length) return - - suggestedUsers.actors.forEach((profile, index) => { - if (!seenProfilesRef.current.has(profile.did)) { - seenProfilesRef.current.add(profile.did) + const onProfileSeen = useCallback( + (did: string, position: number) => { + if (!seenProfilesRef.current.has(did)) { + seenProfilesRef.current.add(did) logger.metric( 'suggestedUser:seen', { logContext: 'Onboarding', recId: undefined, - position: index, + position, + suggestedDid: did, + category: selectedInterest, }, {statsig: true}, ) } - }) - }, [isLoading, moderationOpts, suggestedUsers]) + }, + [selectedInterest], + ) return ( @@ -214,6 +222,8 @@ export function StepSuggestedAccounts() { profile={user} moderationOpts={moderationOpts} position={index} + category={selectedInterest} + onSeen={onProfileSeen} /> ))} @@ -324,14 +334,52 @@ function SuggestedProfileCard({ profile, moderationOpts, position, + category, + onSeen, }: { profile: bsky.profile.AnyProfileView moderationOpts: ModerationOpts position: number + category: string | null + onSeen: (did: string, position: number) => void }) { const t = useTheme() + const cardRef = useRef(null) + const hasTrackedRef = useRef(false) + + useEffect(() => { + const node = cardRef.current + if (!node || hasTrackedRef.current) return + + if (isWeb && typeof IntersectionObserver !== 'undefined') { + const observer = new IntersectionObserver( + entries => { + if (entries[0]?.isIntersecting && !hasTrackedRef.current) { + hasTrackedRef.current = true + onSeen(profile.did, position) + observer.disconnect() + } + }, + {threshold: 0.5}, + ) + // @ts-ignore - web only + observer.observe(node) + return () => observer.disconnect() + } else { + // Native: use a short delay to account for initial layout + const timeout = setTimeout(() => { + if (!hasTrackedRef.current) { + hasTrackedRef.current = true + onSeen(profile.did, position) + } + }, 500) + return () => clearTimeout(timeout) + } + }, [onSeen, profile.did, position]) + return (