Add canonical OP thread numbering (#11184)

Co-authored-by: DS Boyce <260543580+ds-boyce@users.noreply.github.com>
This commit is contained in:
Eric Bailey
2026-08-05 12:20:43 -05:00
committed by GitHub
parent 8cb6c7babe
commit b79908ecea
8 changed files with 164 additions and 17 deletions
+1 -1
View File
@@ -108,7 +108,7 @@
"@bsky.app/expo-scroll-edge-effect": "^0.1.9",
"@bsky.app/expo-translate-text": "^0.2.9",
"@bsky.app/peek-menu": "^0.3.2",
"@bsky.app/react-native-uitextview": "^2.5.0",
"@bsky.app/react-native-uitextview": "^2.6.0",
"@bsky.app/sift": "^0.3.9",
"@bsky.app/tapper": "^0.6.1",
"@bsky.app/video": "0.3.6",
+5 -5
View File
@@ -278,8 +278,8 @@ importers:
specifier: ^0.3.2
version: 0.3.2(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
'@bsky.app/react-native-uitextview':
specifier: ^2.5.0
version: 2.5.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
specifier: ^2.6.0
version: 2.6.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
'@bsky.app/sift':
specifier: ^0.3.9
version: 0.3.9(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
@@ -1676,8 +1676,8 @@ packages:
react: '*'
react-native: '*'
'@bsky.app/react-native-uitextview@2.5.0':
resolution: {integrity: sha512-8DyQAJEkr0ZPEb9QMmT3088V70B6xRsY8e/jvi2oq13hCsLEXCw5M1S48xKRnr8263bJxSH0BjK0nfhPS8Myqw==}
'@bsky.app/react-native-uitextview@2.6.0':
resolution: {integrity: sha512-d6efoPKixj7ycaQWYSDr4JDB1jzIewWie6UtP4Pylgh+K//aXCNaudbLjHN4WP6RVGBfFqNac13hSh3tRikH7A==}
peerDependencies:
react: '*'
react-native: '>=0.81.5'
@@ -10420,7 +10420,7 @@ snapshots:
react: 19.1.0
react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0)
'@bsky.app/react-native-uitextview@2.5.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)':
'@bsky.app/react-native-uitextview@2.6.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)':
dependencies:
react: 19.1.0
react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0)
+1
View File
@@ -24,6 +24,7 @@ export enum Features {
SearchStarterPacksV2Enable = 'search_starter_packs_v2:enable',
FollowSortEnable = 'follow_sort:enable',
OnboardingInterestsRequiredEnable = 'onboarding:interests:required:enable',
CanonicalPostNumberingEnable = 'canonical_post_numbering:enable',
// values
TrendingDiscoverValues = 'trending_discover:values',
+8
View File
@@ -27,6 +27,7 @@ export type RichTextProps = TextStyleProp &
interactiveStyle?: StyleProp<TextStyle>
emojiMultiplier?: number
shouldProxyLinks?: boolean
suffix?: React.ReactNode
/**
* DANGEROUS: Disable facet lexicon validation
*
@@ -54,6 +55,7 @@ export function RichText({
onLayout,
onTextLayout,
shouldProxyLinks,
suffix,
disableMentionFacetValidation,
}: RichTextProps) {
const richText = useMemo(() => {
@@ -87,6 +89,8 @@ export function RichText({
// @ts-ignore web only -prf
dataSet={WORD_WRAP}>
{text}
{suffix ? ' ' : null}
{suffix}
</Text>
)
}
@@ -102,6 +106,8 @@ export function RichText({
// @ts-ignore web only -prf
dataSet={WORD_WRAP}>
{text}
{suffix ? ' ' : null}
{suffix}
</Text>
)
}
@@ -188,6 +194,8 @@ export function RichText({
// @ts-ignore web only -prf
dataSet={WORD_WRAP}>
{els}
{suffix ? ' ' : null}
{suffix}
</Text>
)
}
@@ -30,6 +30,10 @@ import {type PostSource} from '#/state/unstable-post-source'
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
import {KnownLikers, LikesStat} from '#/screens/PostThread/components/LikesStat'
import {ThreadItemAnchorFollowButton} from '#/screens/PostThread/components/ThreadItemAnchorFollowButton'
import {
hasThreadItemPostNumber,
ThreadItemPostNumber,
} from '#/screens/PostThread/components/ThreadItemPostNumber'
import {
LINEAR_AVI_WIDTH,
OUTER_SPACE,
@@ -184,6 +188,7 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
const post = postShadow
const record = item.value.post.record
const postNumbering = item.value
const moderation = item.moderation
const authorShadow = useProfileShadow(post.author)
const {isActive: live} = useActorStatus(post.author)
@@ -400,8 +405,15 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
style={[a.flex_1, a.text_lg]}
authorHandle={post.author.handle}
shouldProxyLinks={true}
suffix={
hasThreadItemPostNumber(postNumbering) ? (
<ThreadItemPostNumber value={postNumbering} />
) : undefined
}
/>
) : undefined}
) : (
<ThreadItemPostNumber inline={false} value={postNumbering} />
)}
<TranslatedPost post={post} postTextStyle={[a.text_lg]} />
{post.embed && (
<View style={[richText?.text ? a.py_xs : []]}>
@@ -23,6 +23,10 @@ import {type OnPostSuccessData} from '#/state/shell/composer'
import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
import {PostMeta} from '#/view/com/util/PostMeta'
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
import {
hasThreadItemPostNumber,
ThreadItemPostNumber,
} from '#/screens/PostThread/components/ThreadItemPostNumber'
import {
LINEAR_AVI_WIDTH,
OUTER_SPACE,
@@ -196,6 +200,7 @@ const ThreadItemPostInner = memo(function ThreadItemPostInner({
const post = item.value.post
const record = item.value.post.record
const postNumbering = item.value
const moderation = item.moderation
const richText = useMemo(
() =>
@@ -324,15 +329,28 @@ const ThreadItemPostInner = memo(function ThreadItemPostInner({
numberOfLines={limitLines ? MAX_POST_LINES : undefined}
authorHandle={post.author.handle}
shouldProxyLinks={true}
suffix={
!limitLines && hasThreadItemPostNumber(postNumbering) ? (
<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} />
)}
<TranslatedPost hideTranslateLink post={post} />
{post.embed && (
<View
@@ -0,0 +1,86 @@
import {Text, View} from 'react-native'
import {type AppBskyUnspeccedDefs} from '@atproto/api'
import {Trans, useLingui} from '@lingui/react/macro'
import {atoms as a, native, platform, useTheme} from '#/alf'
import {useAnalytics} from '#/analytics'
export function hasThreadItemPostNumber(
value: AppBskyUnspeccedDefs.ThreadItemPost,
) {
const index = value.opThreadPostIndex
const count = value.opThreadPostCount
return (
index !== undefined &&
count !== undefined &&
index >= 1 &&
count >= 1 &&
index <= count
)
}
export function ThreadItemPostNumber({
value,
inline = true,
}: {
value: AppBskyUnspeccedDefs.ThreadItemPost
inline?: boolean
}) {
const ax = useAnalytics()
const t = useTheme()
const {t: l} = useLingui()
const index = value.opThreadPostIndex
const count = value.opThreadPostCount
const isEnabled = ax.features.enabled(
ax.features.CanonicalPostNumberingEnable,
)
if (!isEnabled || !hasThreadItemPostNumber(value)) {
return null
}
return (
<View
style={[
a.flex_shrink_0,
a.rounded_full,
t.atoms.bg_contrast_50,
native(a.py_2xs),
{
paddingLeft: 5,
paddingRight: 5,
},
inline
? platform({
native: {transform: [{translateY: 6}]},
web: {top: -2},
})
: {top: -2},
]}>
<Text
accessibilityLabel={l({
message: `Post ${index} of ${count}`,
context: 'post-number-in-thread',
comment:
"Screen reader label indicating post count in a thread, e.g., the 3rd post of 5 total is 'Post 3 of 5'",
})}
accessibilityHint=""
style={[
a.text_xs,
a.font_medium,
t.atoms.text_contrast_high,
{
fontVariant: ['tabular-nums'],
},
]}>
<Trans
context="post-number-in-thread"
comment="Badge indicating post count in a thread, e.g., the 3rd post of 5 total is '3/5'">
{index}/{count}
</Trans>
</Text>
</View>
)
}
@@ -22,6 +22,10 @@ import {useSession} from '#/state/session'
import {type OnPostSuccessData} from '#/state/shell/composer'
import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
import {PostMeta} from '#/view/com/util/PostMeta'
import {
hasThreadItemPostNumber,
ThreadItemPostNumber,
} from '#/screens/PostThread/components/ThreadItemPostNumber'
import {
OUTER_SPACE,
REPLY_LINE_WIDTH,
@@ -258,6 +262,7 @@ const ThreadItemTreePostInner = memo(function ThreadItemTreePostInner({
const post = item.value.post
const record = item.value.post.record
const postNumbering = item.value
const moderation = item.moderation
const richText = useMemo(
() =>
@@ -354,15 +359,32 @@ const ThreadItemTreePostInner = memo(function ThreadItemTreePostInner({
numberOfLines={limitLines ? MAX_POST_LINES : undefined}
authorHandle={post.author.handle}
shouldProxyLinks={true}
suffix={
!limitLines &&
hasThreadItemPostNumber(postNumbering) ? (
<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>
) : null}
) : (
<ThreadItemPostNumber
inline={false}
value={postNumbering}
/>
)}
<TranslatedPost hideTranslateLink post={post} />
{post.embed && (
<View style={[a.pb_xs]}>