Profile follow client events (#9385)
* Add parameters to profile:follow Track who was followed, whose profile generated the follow, and the position of the person who was followed in the list * Add profileCard:seen event * Don't send "profileCard:seen" event to Statsig * Clean up * prevent overzealous clearing --------- Co-authored-by: Samuel Newman <mozzius@protonmail.com>
This commit is contained in:
@@ -48,11 +48,15 @@ export function Default({
|
|||||||
moderationOpts,
|
moderationOpts,
|
||||||
logContext = 'ProfileCard',
|
logContext = 'ProfileCard',
|
||||||
testID,
|
testID,
|
||||||
|
position,
|
||||||
|
contextProfileDid,
|
||||||
}: {
|
}: {
|
||||||
profile: bsky.profile.AnyProfileView
|
profile: bsky.profile.AnyProfileView
|
||||||
moderationOpts: ModerationOpts
|
moderationOpts: ModerationOpts
|
||||||
logContext?: 'ProfileCard' | 'StarterPackProfilesList'
|
logContext?: 'ProfileCard' | 'StarterPackProfilesList'
|
||||||
testID?: string
|
testID?: string
|
||||||
|
position?: number
|
||||||
|
contextProfileDid?: string
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Link testID={testID} profile={profile}>
|
<Link testID={testID} profile={profile}>
|
||||||
@@ -60,6 +64,8 @@ export function Default({
|
|||||||
profile={profile}
|
profile={profile}
|
||||||
moderationOpts={moderationOpts}
|
moderationOpts={moderationOpts}
|
||||||
logContext={logContext}
|
logContext={logContext}
|
||||||
|
position={position}
|
||||||
|
contextProfileDid={contextProfileDid}
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
@@ -69,10 +75,14 @@ export function Card({
|
|||||||
profile,
|
profile,
|
||||||
moderationOpts,
|
moderationOpts,
|
||||||
logContext = 'ProfileCard',
|
logContext = 'ProfileCard',
|
||||||
|
position,
|
||||||
|
contextProfileDid,
|
||||||
}: {
|
}: {
|
||||||
profile: bsky.profile.AnyProfileView
|
profile: bsky.profile.AnyProfileView
|
||||||
moderationOpts: ModerationOpts
|
moderationOpts: ModerationOpts
|
||||||
logContext?: 'ProfileCard' | 'StarterPackProfilesList'
|
logContext?: 'ProfileCard' | 'StarterPackProfilesList'
|
||||||
|
position?: number
|
||||||
|
contextProfileDid?: string
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Outer>
|
<Outer>
|
||||||
@@ -83,6 +93,8 @@ export function Card({
|
|||||||
profile={profile}
|
profile={profile}
|
||||||
moderationOpts={moderationOpts}
|
moderationOpts={moderationOpts}
|
||||||
logContext={logContext}
|
logContext={logContext}
|
||||||
|
position={position}
|
||||||
|
contextProfileDid={contextProfileDid}
|
||||||
/>
|
/>
|
||||||
</Header>
|
</Header>
|
||||||
|
|
||||||
@@ -437,6 +449,8 @@ export type FollowButtonProps = {
|
|||||||
colorInverted?: boolean
|
colorInverted?: boolean
|
||||||
onFollow?: () => void
|
onFollow?: () => void
|
||||||
withIcon?: boolean
|
withIcon?: boolean
|
||||||
|
position?: number
|
||||||
|
contextProfileDid?: string
|
||||||
} & Partial<ButtonProps>
|
} & Partial<ButtonProps>
|
||||||
|
|
||||||
export function FollowButton(props: FollowButtonProps) {
|
export function FollowButton(props: FollowButtonProps) {
|
||||||
@@ -453,6 +467,8 @@ export function FollowButtonInner({
|
|||||||
onFollow,
|
onFollow,
|
||||||
colorInverted,
|
colorInverted,
|
||||||
withIcon = true,
|
withIcon = true,
|
||||||
|
position,
|
||||||
|
contextProfileDid,
|
||||||
...rest
|
...rest
|
||||||
}: FollowButtonProps) {
|
}: FollowButtonProps) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
@@ -461,6 +477,8 @@ export function FollowButtonInner({
|
|||||||
const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(
|
const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(
|
||||||
profile,
|
profile,
|
||||||
logContext,
|
logContext,
|
||||||
|
position,
|
||||||
|
contextProfileDid,
|
||||||
)
|
)
|
||||||
const isRound = Boolean(rest.shape && rest.shape === 'round')
|
const isRound = Boolean(rest.shape && rest.shape === 'round')
|
||||||
|
|
||||||
|
|||||||
@@ -256,9 +256,12 @@ export type MetricEvents = {
|
|||||||
'bookmarks:view': {}
|
'bookmarks:view': {}
|
||||||
'bookmarks:post-clicked': {}
|
'bookmarks:post-clicked': {}
|
||||||
'profile:follow': {
|
'profile:follow': {
|
||||||
|
contextProfileDid?: string
|
||||||
didBecomeMutual: boolean | undefined
|
didBecomeMutual: boolean | undefined
|
||||||
followeeClout: number | undefined
|
followeeClout: number | undefined
|
||||||
|
followeeDid: string
|
||||||
followerClout: number | undefined
|
followerClout: number | undefined
|
||||||
|
position?: number
|
||||||
logContext:
|
logContext:
|
||||||
| 'RecommendedFollowsItem'
|
| 'RecommendedFollowsItem'
|
||||||
| 'PostThreadItem'
|
| 'PostThreadItem'
|
||||||
@@ -276,6 +279,11 @@ export type MetricEvents = {
|
|||||||
| 'ExploreSuggestedAccounts'
|
| 'ExploreSuggestedAccounts'
|
||||||
| 'OnboardingSuggestedAccounts'
|
| 'OnboardingSuggestedAccounts'
|
||||||
}
|
}
|
||||||
|
'profileCard:seen': {
|
||||||
|
contextProfileDid?: string
|
||||||
|
profileDid: string
|
||||||
|
position?: number
|
||||||
|
}
|
||||||
'suggestedUser:follow': {
|
'suggestedUser:follow': {
|
||||||
logContext:
|
logContext:
|
||||||
| 'Explore'
|
| 'Explore'
|
||||||
|
|||||||
@@ -242,12 +242,19 @@ export function useProfileFollowMutationQueue(
|
|||||||
profile: Shadow<bsky.profile.AnyProfileView>,
|
profile: Shadow<bsky.profile.AnyProfileView>,
|
||||||
logContext: LogEvents['profile:follow']['logContext'] &
|
logContext: LogEvents['profile:follow']['logContext'] &
|
||||||
LogEvents['profile:follow']['logContext'],
|
LogEvents['profile:follow']['logContext'],
|
||||||
|
position?: number,
|
||||||
|
contextProfileDid?: string,
|
||||||
) {
|
) {
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const did = profile.did
|
const did = profile.did
|
||||||
const initialFollowingUri = profile.viewer?.following
|
const initialFollowingUri = profile.viewer?.following
|
||||||
const followMutation = useProfileFollowMutation(logContext, profile)
|
const followMutation = useProfileFollowMutation(
|
||||||
|
logContext,
|
||||||
|
profile,
|
||||||
|
position,
|
||||||
|
contextProfileDid,
|
||||||
|
)
|
||||||
const unfollowMutation = useProfileUnfollowMutation(logContext)
|
const unfollowMutation = useProfileUnfollowMutation(logContext)
|
||||||
|
|
||||||
const queueToggle = useToggleMutationQueue({
|
const queueToggle = useToggleMutationQueue({
|
||||||
@@ -314,6 +321,8 @@ export function useProfileFollowMutationQueue(
|
|||||||
function useProfileFollowMutation(
|
function useProfileFollowMutation(
|
||||||
logContext: LogEvents['profile:follow']['logContext'],
|
logContext: LogEvents['profile:follow']['logContext'],
|
||||||
profile: Shadow<bsky.profile.AnyProfileView>,
|
profile: Shadow<bsky.profile.AnyProfileView>,
|
||||||
|
position?: number,
|
||||||
|
contextProfileDid?: string,
|
||||||
) {
|
) {
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
@@ -336,7 +345,10 @@ function useProfileFollowMutation(
|
|||||||
'followersCount' in profile
|
'followersCount' in profile
|
||||||
? toClout(profile.followersCount)
|
? toClout(profile.followersCount)
|
||||||
: undefined,
|
: undefined,
|
||||||
|
followeeDid: did,
|
||||||
followerClout: toClout(ownProfile?.followersCount),
|
followerClout: toClout(ownProfile?.followersCount),
|
||||||
|
position,
|
||||||
|
contextProfileDid,
|
||||||
})
|
})
|
||||||
return await agent.follow(did)
|
return await agent.follow(did)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,10 +9,14 @@ export function ProfileCardWithFollowBtn({
|
|||||||
profile,
|
profile,
|
||||||
noBorder,
|
noBorder,
|
||||||
logContext = 'ProfileCard',
|
logContext = 'ProfileCard',
|
||||||
|
position,
|
||||||
|
contextProfileDid,
|
||||||
}: {
|
}: {
|
||||||
profile: AppBskyActorDefs.ProfileView
|
profile: AppBskyActorDefs.ProfileView
|
||||||
noBorder?: boolean
|
noBorder?: boolean
|
||||||
logContext?: 'ProfileCard' | 'StarterPackProfilesList'
|
logContext?: 'ProfileCard' | 'StarterPackProfilesList'
|
||||||
|
position?: number
|
||||||
|
contextProfileDid?: string
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const moderationOpts = useModerationOpts()
|
const moderationOpts = useModerationOpts()
|
||||||
@@ -30,6 +34,8 @@ export function ProfileCardWithFollowBtn({
|
|||||||
profile={profile}
|
profile={profile}
|
||||||
moderationOpts={moderationOpts}
|
moderationOpts={moderationOpts}
|
||||||
logContext={logContext}
|
logContext={logContext}
|
||||||
|
position={position}
|
||||||
|
contextProfileDid={contextProfileDid}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -16,15 +16,19 @@ import {ProfileCardWithFollowBtn} from './ProfileCard'
|
|||||||
function renderItem({
|
function renderItem({
|
||||||
item,
|
item,
|
||||||
index,
|
index,
|
||||||
|
contextProfileDid,
|
||||||
}: {
|
}: {
|
||||||
item: ActorDefs.ProfileView
|
item: ActorDefs.ProfileView
|
||||||
index: number
|
index: number
|
||||||
|
contextProfileDid: string | undefined
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<ProfileCardWithFollowBtn
|
<ProfileCardWithFollowBtn
|
||||||
key={item.did}
|
key={item.did}
|
||||||
profile={item}
|
profile={item}
|
||||||
noBorder={index === 0}
|
noBorder={index === 0}
|
||||||
|
position={index + 1}
|
||||||
|
contextProfileDid={contextProfileDid}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -83,6 +87,40 @@ export function ProfileFollowers({name}: {name: string}) {
|
|||||||
}
|
}
|
||||||
}, [isFetchingNextPage, hasNextPage, error, fetchNextPage])
|
}, [isFetchingNextPage, hasNextPage, error, fetchNextPage])
|
||||||
|
|
||||||
|
const renderItemWithContext = React.useCallback(
|
||||||
|
({item, index}: {item: ActorDefs.ProfileView; index: number}) =>
|
||||||
|
renderItem({item, index, contextProfileDid: resolvedDid}),
|
||||||
|
[resolvedDid],
|
||||||
|
)
|
||||||
|
|
||||||
|
// track seen items
|
||||||
|
const seenItemsRef = React.useRef<Set<string>>(new Set())
|
||||||
|
React.useEffect(() => {
|
||||||
|
seenItemsRef.current.clear()
|
||||||
|
}, [resolvedDid])
|
||||||
|
const onItemSeen = React.useCallback(
|
||||||
|
(item: ActorDefs.ProfileView) => {
|
||||||
|
if (seenItemsRef.current.has(item.did)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
seenItemsRef.current.add(item.did)
|
||||||
|
const position = followers.findIndex(p => p.did === item.did) + 1
|
||||||
|
if (position === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.metric(
|
||||||
|
'profileCard:seen',
|
||||||
|
{
|
||||||
|
profileDid: item.did,
|
||||||
|
position,
|
||||||
|
...(resolvedDid !== undefined && {contextProfileDid: resolvedDid}),
|
||||||
|
},
|
||||||
|
{statsig: false},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
[followers, resolvedDid],
|
||||||
|
)
|
||||||
|
|
||||||
if (followers.length < 1) {
|
if (followers.length < 1) {
|
||||||
return (
|
return (
|
||||||
<ListMaybePlaceholder
|
<ListMaybePlaceholder
|
||||||
@@ -104,12 +142,13 @@ export function ProfileFollowers({name}: {name: string}) {
|
|||||||
return (
|
return (
|
||||||
<List
|
<List
|
||||||
data={followers}
|
data={followers}
|
||||||
renderItem={renderItem}
|
renderItem={renderItemWithContext}
|
||||||
keyExtractor={keyExtractor}
|
keyExtractor={keyExtractor}
|
||||||
refreshing={isPTRing}
|
refreshing={isPTRing}
|
||||||
onRefresh={onRefresh}
|
onRefresh={onRefresh}
|
||||||
onEndReached={onEndReached}
|
onEndReached={onEndReached}
|
||||||
onEndReachedThreshold={4}
|
onEndReachedThreshold={4}
|
||||||
|
onItemSeen={onItemSeen}
|
||||||
ListFooterComponent={
|
ListFooterComponent={
|
||||||
<ListFooter
|
<ListFooter
|
||||||
isFetchingNextPage={isFetchingNextPage}
|
isFetchingNextPage={isFetchingNextPage}
|
||||||
|
|||||||
@@ -16,15 +16,19 @@ import {ProfileCardWithFollowBtn} from './ProfileCard'
|
|||||||
function renderItem({
|
function renderItem({
|
||||||
item,
|
item,
|
||||||
index,
|
index,
|
||||||
|
contextProfileDid,
|
||||||
}: {
|
}: {
|
||||||
item: ActorDefs.ProfileView
|
item: ActorDefs.ProfileView
|
||||||
index: number
|
index: number
|
||||||
|
contextProfileDid: string | undefined
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<ProfileCardWithFollowBtn
|
<ProfileCardWithFollowBtn
|
||||||
key={item.did}
|
key={item.did}
|
||||||
profile={item}
|
profile={item}
|
||||||
noBorder={index === 0}
|
noBorder={index === 0}
|
||||||
|
position={index + 1}
|
||||||
|
contextProfileDid={contextProfileDid}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -83,6 +87,40 @@ export function ProfileFollows({name}: {name: string}) {
|
|||||||
}
|
}
|
||||||
}, [error, fetchNextPage, hasNextPage, isFetchingNextPage])
|
}, [error, fetchNextPage, hasNextPage, isFetchingNextPage])
|
||||||
|
|
||||||
|
const renderItemWithContext = React.useCallback(
|
||||||
|
({item, index}: {item: ActorDefs.ProfileView; index: number}) =>
|
||||||
|
renderItem({item, index, contextProfileDid: resolvedDid}),
|
||||||
|
[resolvedDid],
|
||||||
|
)
|
||||||
|
|
||||||
|
// track seen items
|
||||||
|
const seenItemsRef = React.useRef<Set<string>>(new Set())
|
||||||
|
React.useEffect(() => {
|
||||||
|
seenItemsRef.current.clear()
|
||||||
|
}, [resolvedDid])
|
||||||
|
const onItemSeen = React.useCallback(
|
||||||
|
(item: ActorDefs.ProfileView) => {
|
||||||
|
if (seenItemsRef.current.has(item.did)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
seenItemsRef.current.add(item.did)
|
||||||
|
const position = follows.findIndex(p => p.did === item.did) + 1
|
||||||
|
if (position === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.metric(
|
||||||
|
'profileCard:seen',
|
||||||
|
{
|
||||||
|
profileDid: item.did,
|
||||||
|
position,
|
||||||
|
...(resolvedDid !== undefined && {contextProfileDid: resolvedDid}),
|
||||||
|
},
|
||||||
|
{statsig: false},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
[follows, resolvedDid],
|
||||||
|
)
|
||||||
|
|
||||||
if (follows.length < 1) {
|
if (follows.length < 1) {
|
||||||
return (
|
return (
|
||||||
<ListMaybePlaceholder
|
<ListMaybePlaceholder
|
||||||
@@ -104,12 +142,13 @@ export function ProfileFollows({name}: {name: string}) {
|
|||||||
return (
|
return (
|
||||||
<List
|
<List
|
||||||
data={follows}
|
data={follows}
|
||||||
renderItem={renderItem}
|
renderItem={renderItemWithContext}
|
||||||
keyExtractor={keyExtractor}
|
keyExtractor={keyExtractor}
|
||||||
refreshing={isPTRing}
|
refreshing={isPTRing}
|
||||||
onRefresh={onRefresh}
|
onRefresh={onRefresh}
|
||||||
onEndReached={onEndReached}
|
onEndReached={onEndReached}
|
||||||
onEndReachedThreshold={4}
|
onEndReachedThreshold={4}
|
||||||
|
onItemSeen={onItemSeen}
|
||||||
ListFooterComponent={
|
ListFooterComponent={
|
||||||
<ListFooter
|
<ListFooter
|
||||||
isFetchingNextPage={isFetchingNextPage}
|
isFetchingNextPage={isFetchingNextPage}
|
||||||
|
|||||||
Reference in New Issue
Block a user