[D1X] Use user action and viewing history to inform suggested follows (#4727)

* Use user action and viewing history to inform suggested follows

* Remove dynamic spreads

* Track more info about seen posts

* Add ranking

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
This commit is contained in:
Eric Bailey 2024-07-04 16:28:38 -05:00 committed by GitHub
parent 1c6bfc02fb
commit 3407206f52
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 196 additions and 49 deletions

View file

@ -17,11 +17,13 @@ import {
import {HomeFeedAPI} from '#/lib/api/feed/home'
import {aggregateUserInterests} from '#/lib/api/feed/utils'
import {DISCOVER_FEED_URI} from '#/lib/constants'
import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
import {logger} from '#/logger'
import {STALE} from '#/state/queries'
import {DEFAULT_LOGGED_OUT_PREFERENCES} from '#/state/queries/preferences/const'
import {useAgent} from '#/state/session'
import * as userActionHistory from '#/state/userActionHistory'
import {AuthorFeedAPI} from 'lib/api/feed/author'
import {CustomFeedAPI} from 'lib/api/feed/custom'
import {FollowingFeedAPI} from 'lib/api/feed/following'
@ -131,6 +133,7 @@ export function usePostFeedQuery(
result: InfiniteData<FeedPage>
} | null>(null)
const lastPageCountRef = useRef(0)
const isDiscover = feedDesc.includes(DISCOVER_FEED_URI)
// Make sure this doesn't invalidate unless really needed.
const selectArgs = React.useMemo(
@ -139,8 +142,15 @@ export function usePostFeedQuery(
disableTuner: params?.disableTuner,
moderationOpts,
ignoreFilterFor: opts?.ignoreFilterFor,
isDiscover,
}),
[feedTuners, params?.disableTuner, moderationOpts, opts?.ignoreFilterFor],
[
feedTuners,
params?.disableTuner,
moderationOpts,
opts?.ignoreFilterFor,
isDiscover,
],
)
const query = useInfiniteQuery<
@ -219,8 +229,13 @@ export function usePostFeedQuery(
(data: InfiniteData<FeedPageUnselected, RQPageParam>) => {
// If the selection depends on some data, that data should
// be included in the selectArgs object and read here.
const {feedTuners, disableTuner, moderationOpts, ignoreFilterFor} =
selectArgs
const {
feedTuners,
disableTuner,
moderationOpts,
ignoreFilterFor,
isDiscover,
} = selectArgs
const tuner = disableTuner
? new NoopFeedTuner()
@ -293,6 +308,21 @@ export function usePostFeedQuery(
}
}
if (isDiscover) {
userActionHistory.seen(
slice.items.map(item => ({
feedContext: item.feedContext,
likeCount: item.post.likeCount ?? 0,
repostCount: item.post.repostCount ?? 0,
replyCount: item.post.replyCount ?? 0,
isFollowedBy: Boolean(
item.post.author.viewer?.followedBy,
),
uri: item.post.uri,
})),
)
}
return {
_reactKey: slice._reactKey,
_isFeedPostSlice: true,

View file

@ -8,6 +8,7 @@ import {logEvent, LogEvents, toClout} from '#/lib/statsig/statsig'
import {updatePostShadow} from '#/state/cache/post-shadow'
import {Shadow} from '#/state/cache/types'
import {useAgent, useSession} from '#/state/session'
import * as userActionHistory from '#/state/userActionHistory'
import {useIsThreadMuted, useSetThreadMute} from '../cache/thread-mutes'
import {findProfileQueryData} from './profile'
@ -92,6 +93,7 @@ export function usePostLikeMutationQueue(
uri: postUri,
cid: postCid,
})
userActionHistory.like([postUri])
return likeUri
} else {
if (prevLikeUri) {
@ -99,6 +101,7 @@ export function usePostLikeMutationQueue(
postUri: postUri,
likeUri: prevLikeUri,
})
userActionHistory.unlike([postUri])
}
return undefined
}

View file

@ -23,6 +23,7 @@ import {logEvent, LogEvents, toClout} from '#/lib/statsig/statsig'
import {Shadow} from '#/state/cache/types'
import {STALE} from '#/state/queries'
import {resetProfilePostsQueries} from '#/state/queries/post-feed'
import * as userActionHistory from '#/state/userActionHistory'
import {updateProfileShadow} from '../cache/profile-shadow'
import {useAgent, useSession} from '../session'
import {
@ -233,6 +234,7 @@ export function useProfileFollowMutationQueue(
const {uri} = await followMutation.mutateAsync({
did,
})
userActionHistory.follow([did])
return uri
} else {
if (prevFollowingUri) {
@ -240,6 +242,7 @@ export function useProfileFollowMutationQueue(
did,
followUri: prevFollowingUri,
})
userActionHistory.unfollow([did])
}
return undefined
}

View file

@ -0,0 +1,71 @@
import React from 'react'
const LIKE_WINDOW = 100
const FOLLOW_WINDOW = 100
const SEEN_WINDOW = 100
export type SeenPost = {
uri: string
likeCount: number
repostCount: number
replyCount: number
isFollowedBy: boolean
feedContext: string | undefined
}
export type UserActionHistory = {
/**
* The last 100 post URIs the user has liked
*/
likes: string[]
/**
* The last 100 DIDs the user has followed
*/
follows: string[]
/**
* The last 100 post URIs the user has seen from the Discover feed only
*/
seen: SeenPost[]
}
const userActionHistory: UserActionHistory = {
likes: [],
follows: [],
seen: [],
}
export function getActionHistory() {
return userActionHistory
}
export function useActionHistorySnapshot() {
return React.useState(() => getActionHistory())[0]
}
export function like(postUris: string[]) {
userActionHistory.likes = userActionHistory.likes
.concat(postUris)
.slice(-LIKE_WINDOW)
}
export function unlike(postUris: string[]) {
userActionHistory.likes = userActionHistory.likes.filter(
uri => !postUris.includes(uri),
)
}
export function follow(dids: string[]) {
userActionHistory.follows = userActionHistory.follows
.concat(dids)
.slice(-FOLLOW_WINDOW)
}
export function unfollow(dids: string[]) {
userActionHistory.follows = userActionHistory.follows.filter(
uri => !dids.includes(uri),
)
}
export function seen(posts: SeenPost[]) {
userActionHistory.seen = userActionHistory.seen
.concat(posts)
.slice(-SEEN_WINDOW)
}