[APP-2864] Use hydrated known likers

This commit is contained in:
vineyardbovines
2026-08-25 09:51:46 -04:00
parent f87fdd2ea2
commit 4aab7e2cb3
5 changed files with 38 additions and 77 deletions
+27
View File
@@ -202,10 +202,37 @@
},
"embeddingDisabled": {
"type": "boolean"
},
"knownLikers": {
"description": "This property is present only in selected cases, as an optimization.",
"type": "ref",
"ref": "#knownLikers"
}
},
"description": "Metadata about the requesting account's relationship with the subject content. Only has meaningful content for authed requests."
},
"knownLikers": {
"type": "object",
"description": "The post's likers whom you also follow",
"required": [
"count",
"actors"
],
"properties": {
"count": {
"type": "integer"
},
"actors": {
"type": "array",
"minLength": 0,
"maxLength": 5,
"items": {
"type": "ref",
"ref": "app.bsky.actor.defs#profileViewBasic"
}
}
}
},
"feedViewPost": {
"type": "object",
"required": [
-1
View File
@@ -17,7 +17,6 @@ export enum Features {
ComposerLanguageDetectionEnable = 'composer:language_detection:enable',
PostGalleryEmbedEnable = 'post_gallery_embed:enable',
PostThreadKnownLikersEnable = 'post_thread:known_likers:enable',
PostThreadKnownLikersFetchEnable = 'post_thread:known_likers:fetch:enable',
CustomLogoJapanEnable = 'custom_logo:japan:enable',
VideoAllow10MinuteEnable = 'video:allow-10-minute:enable',
VideoMultipartUploadEnable = 'video:multipart_upload:enable',
@@ -6,8 +6,6 @@ import {Plural, Trans, useLingui} from '@lingui/react/macro'
import {makeProfileLink} from '#/lib/routes/links'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useLikedBySampleQuery} from '#/state/queries/post-liked-by'
import {useSession} from '#/state/session'
import {atoms as a, useTheme} from '#/alf'
import {AvatarStack} from '#/components/AvatarStack'
import {InlineLinkText, Link} from '#/components/Link'
@@ -59,29 +57,16 @@ export function LikesStat({post}: {post: app.bsky.feed.defs.PostView}) {
* the post's recent likers, renders a face pile plus "Liked by A and B" on
* its own row below the interaction stats line. Renders nothing otherwise.
*
* Known likers are sourced client-side from a single `getLikes` request (100
* likes, the API max per page), so they are a sample of the most recent
* likers, not an exhaustive list.
* Known likers are included in the post viewer state so this can render with
* the rest of the thread, without a separate request or layout shift.
*/
export function KnownLikers({post}: {post: app.bsky.feed.defs.PostView}) {
const t = useTheme()
const {t: l} = useLingui()
const {hasSession, currentAccount} = useSession()
const moderationOpts = useModerationOpts()
const ax = useAnalytics()
const likeCount = post.likeCount ?? 0
/*
* Kill switch for the getLikes sample request itself, separate from the
* display gate below.
*/
const fetchEnabled = ax.features.enabled(
ax.features.PostThreadKnownLikersFetchEnable,
)
const {data} = useLikedBySampleQuery({
uri: fetchEnabled && hasSession && likeCount > 0 ? post.uri : undefined,
})
if (likeCount === 0) return null
const urip = new AtUri(post.uri)
@@ -89,17 +74,14 @@ export function KnownLikers({post}: {post: app.bsky.feed.defs.PostView}) {
const onPressLikedBy = () => ax.metric('post:likedBy:click', {})
const knownLikersAndModeration = moderationOpts
? (data?.likes ?? [])
.map(like => like.actor)
? (post.viewer?.knownLikers?.actors ?? [])
.map(actor => ({
actor,
moderation: moderateProfile(actor, moderationOpts),
}))
.filter(({actor, moderation}) => {
.filter(({moderation}) => {
const modui = moderation.ui('profileList')
const isMe = actor.did === currentAccount?.did
const following = actor.viewer?.following
return !isMe && following && !modui.filter
return !modui.filter
})
: []
-53
View File
@@ -4,11 +4,8 @@ import {
type QueryClient,
type QueryKey,
useInfiniteQuery,
useQuery,
} from '@tanstack/react-query'
import {STALE} from '#/state/queries'
import {createQueryKey} from '#/state/queries/util'
import {useAppviewClient} from '#/state/session'
import {app} from '#/lexicons'
@@ -43,42 +40,6 @@ export function useLikedByQuery(resolvedUri: string | undefined) {
})
}
/**
* The maximum `limit` accepted by `app.bsky.feed.getLikes` in a single
* request.
*/
const SAMPLE_SIZE = 100
const likedBySampleQueryKeyRoot = 'liked-by-sample'
export const createLikedBySampleQueryKey = (args: {uri: string}) =>
createQueryKey(likedBySampleQueryKeyRoot, args)
/**
* A single-request sample of a post's most recent likers, as many as the API
* allows in one page (100). Used for the known-likers social proof on the
* post thread page. Kept separate from `useLikedByQuery` so it does not
* perturb the liked-by screen's pagination.
*/
export function useLikedBySampleQuery({uri}: {uri: string | undefined}) {
const client = useAppviewClient()
return useQuery({
queryKey: createLikedBySampleQueryKey({uri: uri ?? ''}),
queryFn: async () => {
return await client.call(app.bsky.feed.getLikes, {
uri: (uri ?? '') as AtUriString,
limit: SAMPLE_SIZE,
})
},
staleTime: STALE.MINUTES.FIVE,
enabled: !!uri,
/*
* Consumers fall back to a plain like count when this query fails, so
* failing fast is preferable to amplifying getLikes load with retries.
*/
retry: 1,
})
}
export function* findAllProfilesInQueryData(
queryClient: QueryClient,
did: string,
@@ -100,18 +61,4 @@ export function* findAllProfilesInQueryData(
}
}
}
const sampleQueryDatas =
queryClient.getQueriesData<app.bsky.feed.getLikes.$OutputBody>({
queryKey: [likedBySampleQueryKeyRoot],
})
for (const [_queryKey, queryData] of sampleQueryDatas) {
if (!queryData?.likes) {
continue
}
for (const like of queryData.likes) {
if (like.actor.did === did) {
yield like.actor
}
}
}
}
@@ -325,6 +325,12 @@ export function* findAllProfilesInQueryData(
yield item.value.post.author
}
for (const actor of item.value.post.viewer?.knownLikers?.actors ?? []) {
if (actor.did === did) {
yield actor
}
}
const qp = getEmbeddedPost(item.value.post.embed)
if (qp && qp.author.did === did) {
yield qp.author