diff --git a/lexicons/app/bsky/feed/defs.json b/lexicons/app/bsky/feed/defs.json index 436782d4ec..a59b32914d 100644 --- a/lexicons/app/bsky/feed/defs.json +++ b/lexicons/app/bsky/feed/defs.json @@ -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": [ diff --git a/src/analytics/features/types.ts b/src/analytics/features/types.ts index d86ad83ff7..b08d3c55fa 100644 --- a/src/analytics/features/types.ts +++ b/src/analytics/features/types.ts @@ -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', diff --git a/src/screens/PostThread/components/LikesStat.tsx b/src/screens/PostThread/components/LikesStat.tsx index fab45ab155..15cbeba3ac 100644 --- a/src/screens/PostThread/components/LikesStat.tsx +++ b/src/screens/PostThread/components/LikesStat.tsx @@ -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 }) : [] diff --git a/src/state/queries/post-liked-by.ts b/src/state/queries/post-liked-by.ts index 0964d0be24..b59c5f55c0 100644 --- a/src/state/queries/post-liked-by.ts +++ b/src/state/queries/post-liked-by.ts @@ -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({ - 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 - } - } - } } diff --git a/src/state/queries/usePostThread/queryCache.ts b/src/state/queries/usePostThread/queryCache.ts index afc7465ceb..42c2a05fc9 100644 --- a/src/state/queries/usePostThread/queryCache.ts +++ b/src/state/queries/usePostThread/queryCache.ts @@ -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