diff options
author | Eric Bailey <git@esb.lol> | 2024-07-04 16:28:38 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-07-04 22:28:38 +0100 |
commit | 3407206f52a03223b9eba925f030cf371833a8ed (patch) | |
tree | 33060612a4a5b23232d85b966906ad7f0a1ba35d /src/components/FeedInterstitials.tsx | |
parent | 1c6bfc02fb9da56281bdc449a951725fb2ec808d (diff) | |
download | voidsky-3407206f52a03223b9eba925f030cf371833a8ed.tar.zst |
[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>
Diffstat (limited to 'src/components/FeedInterstitials.tsx')
-rw-r--r-- | src/components/FeedInterstitials.tsx | 105 |
1 files changed, 85 insertions, 20 deletions
diff --git a/src/components/FeedInterstitials.tsx b/src/components/FeedInterstitials.tsx index ca3b085b9..043a27c29 100644 --- a/src/components/FeedInterstitials.tsx +++ b/src/components/FeedInterstitials.tsx @@ -1,7 +1,7 @@ import React from 'react' import {View} from 'react-native' import {ScrollView} from 'react-native-gesture-handler' -import {AppBskyActorDefs, AppBskyFeedDefs} from '@atproto/api' +import {AppBskyFeedDefs, AtUri} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useNavigation} from '@react-navigation/native' @@ -9,10 +9,13 @@ import {useNavigation} from '@react-navigation/native' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {NavigationProp} from '#/lib/routes/types' import {logEvent} from '#/lib/statsig/statsig' +import {logger} from '#/logger' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useGetPopularFeedsQuery} from '#/state/queries/feed' -import {useSuggestedFollowsQuery} from '#/state/queries/suggested-follows' +import {useProfilesQuery} from '#/state/queries/profile' import {useProgressGuide} from '#/state/shell/progress-guide' +import * as userActionHistory from '#/state/userActionHistory' +import {SeenPost} from '#/state/userActionHistory' import {atoms as a, useBreakpoints, useTheme, ViewStyleProp, web} from '#/alf' import {Button} from '#/components/Button' import * as FeedCard from '#/components/FeedCard' @@ -80,35 +83,92 @@ export function SuggestedFeedsCardPlaceholder() { ) } +function getRank(seenPost: SeenPost): string { + let tier: string + if (seenPost.feedContext === 'popfriends') { + tier = 'a' + } else if (seenPost.feedContext?.startsWith('cluster')) { + tier = 'b' + } else if (seenPost.feedContext?.startsWith('ntpc')) { + tier = 'c' + } else if (seenPost.feedContext?.startsWith('t-')) { + tier = 'd' + } else if (seenPost.feedContext === 'nettop') { + tier = 'e' + } else { + tier = 'f' + } + let score = Math.round( + Math.log( + 1 + seenPost.likeCount + seenPost.repostCount + seenPost.replyCount, + ), + ) + if (seenPost.isFollowedBy || Math.random() > 0.9) { + score *= 2 + } + const rank = 100 - score + return `${tier}-${rank}` +} + +function sortSeenPosts(postA: SeenPost, postB: SeenPost): 0 | 1 | -1 { + const rankA = getRank(postA) + const rankB = getRank(postB) + // Yes, we're comparing strings here. + // The "larger" string means a worse rank. + if (rankA > rankB) { + return 1 + } else if (rankA < rankB) { + return -1 + } else { + return 0 + } +} + +function useExperimentalSuggestedUsersQuery() { + const userActionSnapshot = userActionHistory.useActionHistorySnapshot() + const dids = React.useMemo(() => { + const {likes, follows, seen} = userActionSnapshot + const likeDids = likes + .map(l => new AtUri(l)) + .map(uri => uri.host) + .filter(did => !follows.includes(did)) + const seenDids = seen + .sort(sortSeenPosts) + .map(l => new AtUri(l.uri)) + .map(uri => uri.host) + return [...new Set([...likeDids, ...seenDids])] + }, [userActionSnapshot]) + const {data, isLoading, error} = useProfilesQuery({ + handles: dids.slice(0, 16), + }) + + const profiles = data + ? data.profiles.filter(profile => { + return !profile.viewer?.following + }) + : [] + + return { + isLoading, + error, + profiles: profiles.slice(0, 6), + } +} + export function SuggestedFollows() { const t = useTheme() const {_} = useLingui() const { isLoading: isSuggestionsLoading, - data, + profiles, error, - } = useSuggestedFollowsQuery({limit: 6}) + } = useExperimentalSuggestedUsersQuery() const moderationOpts = useModerationOpts() const navigation = useNavigation<NavigationProp>() const {gtMobile} = useBreakpoints() const isLoading = isSuggestionsLoading || !moderationOpts const maxLength = gtMobile ? 4 : 6 - const profiles: AppBskyActorDefs.ProfileViewBasic[] = [] - if (data) { - // Currently the responses contain duplicate items. - // Needs to be fixed on backend, but let's dedupe to be safe. - let seen = new Set() - for (const page of data.pages) { - for (const actor of page.actors) { - if (!seen.has(actor.did)) { - seen.add(actor.did) - profiles.push(actor) - } - } - } - } - const content = isLoading ? ( Array(maxLength) .fill(0) @@ -164,7 +224,12 @@ export function SuggestedFollows() { </> ) - return error ? null : ( + if (error || (!isLoading && profiles.length < 4)) { + logger.debug(`Not enough profiles to show suggested follows`) + return null + } + + return ( <View style={[a.border_t, t.atoms.border_contrast_low, t.atoms.bg_contrast_25]}> <View style={[a.pt_2xl, a.px_lg, a.flex_row, a.pb_xs]}> |