Add OP thread numbering to feed posts (#11472)

This commit is contained in:
DS Boyce
2026-08-25 11:48:27 -07:00
committed by GitHub
parent 7750882fd0
commit f968c9e721
7 changed files with 122 additions and 13 deletions
+69 -3
View File
@@ -4,7 +4,54 @@ import {isPostInLanguage} from '../../locale/helpers'
import {FALLBACK_MARKER_POST} from './feed/home'
import {type ReasonFeedSource} from './feed/types'
type FeedViewPost = app.bsky.feed.defs.FeedViewPost
export type FeedPostNumbering = Pick<
app.bsky.unspecced.defs.ThreadItemPost,
'opThreadPostIndex' | 'opThreadPostCount'
>
type ValidFeedPostNumbering = Required<FeedPostNumbering>
// AppView adds these fields to feed responses ahead of their feed lexicon.
type FeedViewPost = app.bsky.feed.defs.FeedViewPost & FeedPostNumbering
function getPostNumbering(
value: FeedPostNumbering,
): ValidFeedPostNumbering | undefined {
const {opThreadPostIndex: index, opThreadPostCount: count} = value
if (
index === undefined ||
count === undefined ||
index < 1 ||
count < 1 ||
index > count
) {
return undefined
}
return {
opThreadPostIndex: index,
opThreadPostCount: count,
}
}
function inferPostNumbering(
feedPost: FeedViewPost,
position: 'parent' | 'root',
): ValidFeedPostNumbering | undefined {
const postNumbering = getPostNumbering(feedPost)
if (!postNumbering) {
return undefined
}
// Feed responses number only the selected post, so derive the hydrated
// context that the feed renders alongside it.
return getPostNumbering({
opThreadPostIndex:
position === 'root' ? 1 : postNumbering.opThreadPostIndex - 1,
opThreadPostCount: postNumbering.opThreadPostCount,
})
}
export type FeedTunerFn = (
tuner: FeedTuner,
@@ -15,6 +62,7 @@ export type FeedTunerFn = (
type FeedSliceItem = {
post: app.bsky.feed.defs.PostView
record: app.bsky.feed.post.Main
postNumbering: FeedPostNumbering | undefined
parentAuthor: app.bsky.actor.defs.ProfileViewBasic | undefined
isParentBlocked: boolean
isParentNotFound: boolean
@@ -38,7 +86,10 @@ export class FeedViewPostsSlice {
rootUri: string
feedPostUri: string
constructor(feedPost: FeedViewPost) {
constructor(
feedPost: FeedViewPost,
postNumberingByUri: Map<string, FeedPostNumbering>,
) {
const {post, reply, reason} = feedPost
this.items = []
this.isIncompleteThread = false
@@ -80,6 +131,7 @@ export class FeedViewPostsSlice {
this.items.push({
post,
record: post.record,
postNumbering: postNumberingByUri.get(post.uri),
parentAuthor,
isParentBlocked,
isParentNotFound,
@@ -128,6 +180,9 @@ export class FeedViewPostsSlice {
this.items.unshift({
post: parent,
record: parent.record,
postNumbering:
postNumberingByUri.get(parent.uri) ??
inferPostNumbering(feedPost, 'parent'),
parentAuthor: grandparentAuthor,
isParentBlocked: isGrandparentBlocked,
isParentNotFound: isGrandparentNotFound,
@@ -151,6 +206,9 @@ export class FeedViewPostsSlice {
this.items.unshift({
post: root,
record: root.record,
postNumbering:
postNumberingByUri.get(root.uri) ??
inferPostNumbering(feedPost, 'root'),
isParentBlocked: false,
isParentNotFound: false,
parentAuthor: undefined,
@@ -241,8 +299,16 @@ export class FeedTuner {
dryRun: false,
},
): FeedViewPostsSlice[] {
const postNumberingByUri = new Map<string, FeedPostNumbering>()
for (const item of feed) {
const postNumbering = getPostNumbering(item)
if (postNumbering) {
postNumberingByUri.set(item.post.uri, postNumbering)
}
}
let slices: FeedViewPostsSlice[] = feed
.map(item => new FeedViewPostsSlice(item))
.map(item => new FeedViewPostsSlice(item, postNumberingByUri))
.filter(s => s.items.length > 0 || s.isFallbackMarker)
// run the custom tuners
@@ -11,12 +11,17 @@ import {type app} from '#/lexicons'
*/
export const POST_NUMBER_INLINE_OFFSET = 6
export type ThreadItemPostNumbering = Pick<
app.bsky.unspecced.defs.ThreadItemPost,
'opThreadPostIndex' | 'opThreadPostCount'
>
export function useHasThreadItemPostNumber(
value: app.bsky.unspecced.defs.ThreadItemPost,
value: ThreadItemPostNumbering | undefined,
) {
const ax = useAnalytics()
const index = value.opThreadPostIndex
const count = value.opThreadPostCount
const index = value?.opThreadPostIndex
const count = value?.opThreadPostCount
return (
ax.features.enabled(ax.features.CanonicalPostNumberingEnable) &&
@@ -32,14 +37,14 @@ export function ThreadItemPostNumber({
value,
inline = true,
}: {
value: app.bsky.unspecced.defs.ThreadItemPost
value: ThreadItemPostNumbering | undefined
inline?: boolean
}) {
const t = useTheme()
const {t: l} = useLingui()
const shouldRender = useHasThreadItemPostNumber(value)
const index = value.opThreadPostIndex
const count = value.opThreadPostCount
const index = value?.opThreadPostIndex
const count = value?.opThreadPostCount
if (!shouldRender) {
return null
@@ -59,7 +64,8 @@ export function ThreadItemPostNumber({
},
inline
? platform({
native: {transform: [{translateY: POST_NUMBER_INLINE_OFFSET}]},
android: {transform: [{translateY: POST_NUMBER_INLINE_OFFSET}]},
ios: {transform: [{translateY: a.py_2xs.paddingBottom}]},
web: {
top: -2,
marginBottom: -2,
+1
View File
@@ -984,6 +984,7 @@ export function Explore({
<PostFeedItem
post={subItem.post}
record={subItem.record}
postNumbering={subItem.postNumbering}
reason={indexInSlice === 0 ? slice.reason : undefined}
feedContext={slice.feedContext}
reqId={slice.reqId}
@@ -233,6 +233,7 @@ export function useFeedPreviews(
uri: subItem.post.uri,
post: subItem.post,
record: subItem.record,
postNumbering: subItem.postNumbering,
moderation: moderations[i],
parentAuthor: subItem.parentAuthor,
isParentBlocked: subItem.isParentBlocked,
+7 -1
View File
@@ -25,7 +25,11 @@ import {MergeFeedAPI} from '#/lib/api/feed/merge'
import {PostListFeedAPI} from '#/lib/api/feed/posts'
import {type FeedAPI, type ReasonFeedSource} from '#/lib/api/feed/types'
import {aggregateUserInterests} from '#/lib/api/feed/utils'
import {FeedTuner, type FeedTunerFn} from '#/lib/api/feed-manip'
import {
type FeedPostNumbering,
FeedTuner,
type FeedTunerFn,
} from '#/lib/api/feed-manip'
import {DISCOVER_FEED_URI} from '#/lib/constants'
import {logger} from '#/logger'
import {STALE} from '#/state/queries'
@@ -81,6 +85,7 @@ export interface FeedPostSliceItem {
uri: AtUriString
post: app.bsky.feed.defs.PostView
record: app.bsky.feed.post.Main
postNumbering?: FeedPostNumbering
moderation: ModerationDecision
parentAuthor?: app.bsky.actor.defs.ProfileViewBasic
isParentBlocked?: boolean
@@ -340,6 +345,7 @@ export function usePostFeedQuery(
uri: item.post.uri,
post: item.post,
record: item.record,
postNumbering: item.postNumbering,
moderation: moderations[i],
parentAuthor: item.parentAuthor,
isParentBlocked: item.isParentBlocked,
+1
View File
@@ -872,6 +872,7 @@ let PostFeed = ({
<PostFeedItem
post={item.post}
record={item.record}
postNumbering={item.postNumbering}
reason={indexInSlice === 0 ? slice.reason : undefined}
feedContext={slice.feedContext}
reqId={slice.reqId}
+30 -2
View File
@@ -6,6 +6,7 @@ import {RichText as RichTextAPI} from '@bsky/sdk/richtext'
import {useQueryClient} from '@tanstack/react-query'
import {type ReasonFeedSource} from '#/lib/api/feed/types'
import {type FeedPostNumbering} from '#/lib/api/feed-manip'
import {MAX_POST_LINES} from '#/lib/constants'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {usePalette} from '#/lib/hooks/usePalette'
@@ -27,6 +28,11 @@ import {
import {Link} from '#/view/com/util/Link'
import {PostMeta} from '#/view/com/util/PostMeta'
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
import {
POST_NUMBER_INLINE_OFFSET,
ThreadItemPostNumber,
useHasThreadItemPostNumber,
} from '#/screens/PostThread/components/ThreadItemPostNumber'
import {atoms as a, select, useTheme} from '#/alf'
import {
GalleryBleed,
@@ -53,6 +59,7 @@ import {PostFeedReason} from './PostFeedReason'
interface FeedItemProps {
record: app.bsky.feed.post.Main
postNumbering?: FeedPostNumbering
reason:
| app.bsky.feed.defs.ReasonRepost
| app.bsky.feed.defs.ReasonPin
@@ -75,6 +82,7 @@ interface FeedItemProps {
export function PostFeedItem({
post,
record,
postNumbering,
reason,
feedContext,
reqId,
@@ -112,6 +120,7 @@ export function PostFeedItem({
<FeedItemInner
post={postShadowed}
record={record}
postNumbering={postNumbering}
reason={reason}
feedContext={feedContext}
reqId={reqId}
@@ -137,6 +146,7 @@ export function PostFeedItem({
let FeedItemInner = ({
post,
record,
postNumbering,
reason,
feedContext,
reqId,
@@ -426,6 +436,7 @@ let FeedItemInner = ({
<PostContent
moderation={moderation}
richText={richText}
postNumbering={postNumbering}
postEmbed={post.embed}
postAuthor={post.author}
onOpenEmbed={onOpenEmbed}
@@ -457,6 +468,7 @@ FeedItemInner = memo(FeedItemInner)
let PostContent = ({
post,
postNumbering,
moderation,
richText,
postEmbed,
@@ -471,12 +483,14 @@ let PostContent = ({
postAuthor: app.bsky.feed.defs.PostView['author']
onOpenEmbed: () => void
post: app.bsky.feed.defs.PostView
postNumbering: FeedPostNumbering | undefined
additionalPostAlerts?: AppModerationCause[]
feedDescriptor?: string
}): React.ReactNode => {
const [limitLines, setLimitLines] = useState(
() => countLines(richText.text) >= MAX_POST_LINES,
)
const showPostNumber = useHasThreadItemPostNumber(postNumbering)
const record = useMemo<app.bsky.feed.post.Main | undefined>(
() =>
@@ -510,12 +524,26 @@ let PostContent = ({
style={[a.flex_1, a.text_md]}
authorHandle={postAuthor.handle}
shouldProxyLinks={true}
suffixOffset={POST_NUMBER_INLINE_OFFSET}
suffix={
!limitLines && showPostNumber ? (
<ThreadItemPostNumber value={postNumbering} />
) : undefined
}
/>
{limitLines && (
<ShowMoreTextButton style={[a.text_md]} onPress={onPressShowMore} />
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
<ShowMoreTextButton
style={[a.text_md]}
onPress={onPressShowMore}
/>
<ThreadItemPostNumber inline={false} value={postNumbering} />
</View>
)}
</View>
) : undefined}
) : (
<ThreadItemPostNumber inline={false} value={postNumbering} />
)}
{record && <TranslatedPost hideTranslateLink post={post} />}
{postEmbed ? (
<View