Bump app.bsky.feed.getLikes request sample size to 100
This commit is contained in:
@@ -13,6 +13,7 @@ export enum Features {
|
|||||||
ComposerLanguageDetectionEnable = 'composer:language_detection:enable',
|
ComposerLanguageDetectionEnable = 'composer:language_detection:enable',
|
||||||
PostGalleryEmbedEnable = 'post_gallery_embed:enable',
|
PostGalleryEmbedEnable = 'post_gallery_embed:enable',
|
||||||
NotificationsExpandedProfileCardEnable = 'notifications:expanded_profile_card:enable',
|
NotificationsExpandedProfileCardEnable = 'notifications:expanded_profile_card:enable',
|
||||||
|
PostThreadKnownLikersEnable = 'post_thread:known_likers:enable',
|
||||||
SearchV2Enable = 'search_v2:enable',
|
SearchV2Enable = 'search_v2:enable',
|
||||||
AdvancedSearchV2Enable = 'advanced_search_v2:enable',
|
AdvancedSearchV2Enable = 'advanced_search_v2:enable',
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
import {View} from 'react-native'
|
||||||
import {type AppBskyFeedDefs, AtUri, moderateProfile} from '@atproto/api'
|
import {type AppBskyFeedDefs, AtUri, moderateProfile} from '@atproto/api'
|
||||||
import {Plural, Trans, useLingui} from '@lingui/react/macro'
|
import {Plural, Trans, useLingui} from '@lingui/react/macro'
|
||||||
|
|
||||||
import {makeProfileLink} from '#/lib/routes/links'
|
import {makeProfileLink} from '#/lib/routes/links'
|
||||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
import {useLikedByQuery} from '#/state/queries/post-liked-by'
|
import {useLikedBySampleQuery} from '#/state/queries/post-liked-by'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {AvatarStack} from '#/components/AvatarStack'
|
import {AvatarStack} from '#/components/AvatarStack'
|
||||||
@@ -12,6 +13,7 @@ import {InlineLinkText, Link} from '#/components/Link'
|
|||||||
import {useFormatPostStatCount} from '#/components/PostControls/util'
|
import {useFormatPostStatCount} from '#/components/PostControls/util'
|
||||||
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
|
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
|
|
||||||
const AVI_SIZE = 20
|
const AVI_SIZE = 20
|
||||||
|
|
||||||
@@ -21,10 +23,11 @@ const AVI_SIZE = 20
|
|||||||
* "Liked by A, B, and N others" - in place of the plain "N likes" text,
|
* "Liked by A, B, and N others" - in place of the plain "N likes" text,
|
||||||
* which it falls back to otherwise.
|
* which it falls back to otherwise.
|
||||||
*
|
*
|
||||||
* Known likers are sourced client-side from the first page of `getLikes`, so
|
* Known likers are sourced client-side from a single `getLikes` request (100
|
||||||
* they are a sample of the most recent likers, not an exhaustive list. Only
|
* likes, the API max per page), so they are a sample of the most recent
|
||||||
* the faces and names are affected by sampling - the "N others" count is
|
* likers, not an exhaustive list. Only the faces and names are affected by
|
||||||
* derived from the post's total like count.
|
* sampling - the "N others" count is derived from the post's total like
|
||||||
|
* count.
|
||||||
*/
|
*/
|
||||||
export function LikesStat({post}: {post: AppBskyFeedDefs.PostView}) {
|
export function LikesStat({post}: {post: AppBskyFeedDefs.PostView}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
@@ -32,33 +35,33 @@ export function LikesStat({post}: {post: AppBskyFeedDefs.PostView}) {
|
|||||||
const {hasSession, currentAccount} = useSession()
|
const {hasSession, currentAccount} = useSession()
|
||||||
const moderationOpts = useModerationOpts()
|
const moderationOpts = useModerationOpts()
|
||||||
const formatPostStatCount = useFormatPostStatCount()
|
const formatPostStatCount = useFormatPostStatCount()
|
||||||
|
const ax = useAnalytics()
|
||||||
|
const knownLikersEnabled = ax.features.enabled(
|
||||||
|
ax.features.PostThreadKnownLikersEnable,
|
||||||
|
)
|
||||||
|
|
||||||
const likeCount = post.likeCount ?? 0
|
const likeCount = post.likeCount ?? 0
|
||||||
const enabled = hasSession && likeCount > 0
|
const enabled = knownLikersEnabled && hasSession && likeCount > 0
|
||||||
const {data} = useLikedByQuery(enabled ? post.uri : undefined)
|
const {data} = useLikedBySampleQuery({uri: enabled ? post.uri : undefined})
|
||||||
|
|
||||||
if (likeCount === 0) return null
|
if (likeCount === 0) return null
|
||||||
|
|
||||||
const urip = new AtUri(post.uri)
|
const urip = new AtUri(post.uri)
|
||||||
const likesHref = makeProfileLink(post.author, 'post', urip.rkey, 'liked-by')
|
const likesHref = makeProfileLink(post.author, 'post', urip.rkey, 'liked-by')
|
||||||
|
|
||||||
/*
|
const knownLikers =
|
||||||
* Only the first page, even if the liked-by screen has loaded more into
|
knownLikersEnabled && moderationOpts
|
||||||
* this same query cache. This keeps the sample bounds consistent and
|
? (data?.likes ?? [])
|
||||||
* avoids reflowing the row as more pages arrive.
|
.map(like => like.actor)
|
||||||
*/
|
.filter(
|
||||||
const knownLikers = moderationOpts
|
actor =>
|
||||||
? (data?.pages[0]?.likes ?? [])
|
actor.did !== currentAccount?.did &&
|
||||||
.map(like => like.actor)
|
actor.viewer?.following &&
|
||||||
.filter(
|
!actor.viewer.muted &&
|
||||||
actor =>
|
!actor.viewer.blocking &&
|
||||||
actor.did !== currentAccount?.did &&
|
!actor.viewer.blockedBy,
|
||||||
actor.viewer?.following &&
|
)
|
||||||
!actor.viewer.muted &&
|
: []
|
||||||
!actor.viewer.blocking &&
|
|
||||||
!actor.viewer.blockedBy,
|
|
||||||
)
|
|
||||||
: []
|
|
||||||
|
|
||||||
if (knownLikers.length === 0) {
|
if (knownLikers.length === 0) {
|
||||||
return (
|
return (
|
||||||
@@ -112,24 +115,41 @@ export function LikesStat({post}: {post: AppBskyFeedDefs.PostView}) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
/*
|
||||||
to={likesHref}
|
* The full-width wrapper keeps the social proof on its own line within
|
||||||
label={l`Likes on this post`}
|
* the wrapping stats row, rather than wrapping mid-row and orphaning
|
||||||
/*
|
* whichever count stat comes last. The link itself hugs its content so
|
||||||
* Full width so the social proof always sits on its own line within
|
* the empty space to the right of the text is not pressable.
|
||||||
* the wrapping stats row, rather than wrapping mid-row and orphaning
|
*/
|
||||||
* whichever count stat comes last.
|
<View style={[a.w_full, a.flex_row]}>
|
||||||
*/
|
<Link
|
||||||
style={[a.w_full, a.flex_row, a.align_center, a.gap_sm]}>
|
to={likesHref}
|
||||||
<AvatarStack profiles={knownLikers.slice(0, 3)} size={AVI_SIZE} />
|
label={l`Likes on this post`}
|
||||||
<Text
|
style={[a.flex_row, a.align_center, a.gap_sm, a.flex_shrink]}>
|
||||||
testID="knownLikersStat"
|
<AvatarStack profiles={knownLikers.slice(0, 3)} size={AVI_SIZE} />
|
||||||
numberOfLines={1}
|
<Text
|
||||||
style={[a.flex_shrink, textStyle]}>
|
testID="knownLikersStat"
|
||||||
{names.length >= 2 ? (
|
numberOfLines={1}
|
||||||
others > 0 ? (
|
style={[a.flex_shrink, textStyle]}>
|
||||||
<Trans comment="Social proof on the likes stat; the bolded names are people the viewer follows who liked the post, and the count is the remaining number of likes">
|
{names.length >= 2 ? (
|
||||||
{nameLink(names[0])}, {nameLink(names[1])}, and{' '}
|
others > 0 ? (
|
||||||
|
<Trans comment="Social proof on the likes stat; the bolded names are people the viewer follows who liked the post, and the count is the remaining number of likes">
|
||||||
|
{nameLink(names[0])}, {nameLink(names[1])}, and{' '}
|
||||||
|
<Plural
|
||||||
|
value={others}
|
||||||
|
one={`${formatPostStatCount(others)} other`}
|
||||||
|
other={`${formatPostStatCount(others)} others`}
|
||||||
|
/>{' '}
|
||||||
|
like this
|
||||||
|
</Trans>
|
||||||
|
) : (
|
||||||
|
<Trans comment="Social proof on the likes stat; the bolded names are people the viewer follows who liked the post and are its only likes">
|
||||||
|
{nameLink(names[0])} and {nameLink(names[1])} like this
|
||||||
|
</Trans>
|
||||||
|
)
|
||||||
|
) : others > 0 ? (
|
||||||
|
<Trans comment="Social proof on the likes stat; the bolded name is a person the viewer follows who liked the post, and the count is the remaining number of likes">
|
||||||
|
{nameLink(names[0])} and{' '}
|
||||||
<Plural
|
<Plural
|
||||||
value={others}
|
value={others}
|
||||||
one={`${formatPostStatCount(others)} other`}
|
one={`${formatPostStatCount(others)} other`}
|
||||||
@@ -138,26 +158,12 @@ export function LikesStat({post}: {post: AppBskyFeedDefs.PostView}) {
|
|||||||
like this
|
like this
|
||||||
</Trans>
|
</Trans>
|
||||||
) : (
|
) : (
|
||||||
<Trans comment="Social proof on the likes stat; the bolded names are people the viewer follows who liked the post and are its only likes">
|
<Trans comment="Social proof on the likes stat; the bolded name is a person the viewer follows who liked the post and is its only like">
|
||||||
{nameLink(names[0])} and {nameLink(names[1])} like this
|
{nameLink(names[0])} likes this
|
||||||
</Trans>
|
</Trans>
|
||||||
)
|
)}
|
||||||
) : others > 0 ? (
|
</Text>
|
||||||
<Trans comment="Social proof on the likes stat; the bolded name is a person the viewer follows who liked the post, and the count is the remaining number of likes">
|
</Link>
|
||||||
{nameLink(names[0])} and{' '}
|
</View>
|
||||||
<Plural
|
|
||||||
value={others}
|
|
||||||
one={`${formatPostStatCount(others)} other`}
|
|
||||||
other={`${formatPostStatCount(others)} others`}
|
|
||||||
/>{' '}
|
|
||||||
like this
|
|
||||||
</Trans>
|
|
||||||
) : (
|
|
||||||
<Trans comment="Social proof on the likes stat; the bolded name is a person the viewer follows who liked the post and is its only like">
|
|
||||||
{nameLink(names[0])} likes this
|
|
||||||
</Trans>
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
</Link>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,11 @@ import {
|
|||||||
type QueryClient,
|
type QueryClient,
|
||||||
type QueryKey,
|
type QueryKey,
|
||||||
useInfiniteQuery,
|
useInfiniteQuery,
|
||||||
|
useQuery,
|
||||||
} from '@tanstack/react-query'
|
} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import {STALE} from '#/state/queries'
|
||||||
|
import {createQueryKey} from '#/state/queries/util'
|
||||||
import {useAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
|
|
||||||
const PAGE_SIZE = 30
|
const PAGE_SIZE = 30
|
||||||
@@ -39,6 +42,35 @@ 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 agent = useAgent()
|
||||||
|
return useQuery({
|
||||||
|
queryKey: createLikedBySampleQueryKey({uri: uri ?? ''}),
|
||||||
|
queryFn: async () => {
|
||||||
|
const res = await agent.getLikes({uri: uri ?? '', limit: SAMPLE_SIZE})
|
||||||
|
return res.data
|
||||||
|
},
|
||||||
|
staleTime: STALE.MINUTES.FIVE,
|
||||||
|
enabled: !!uri,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function* findAllProfilesInQueryData(
|
export function* findAllProfilesInQueryData(
|
||||||
queryClient: QueryClient,
|
queryClient: QueryClient,
|
||||||
did: string,
|
did: string,
|
||||||
@@ -60,4 +92,18 @@ export function* findAllProfilesInQueryData(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const sampleQueryDatas =
|
||||||
|
queryClient.getQueriesData<AppBskyFeedGetLikes.OutputSchema>({
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user