diff --git a/package.json b/package.json index f49c831c97..2759a59119 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7952820f4d..1ec1f5267c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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) diff --git a/src/analytics/features/types.ts b/src/analytics/features/types.ts index 62fe079b7b..4e8d183c8d 100644 --- a/src/analytics/features/types.ts +++ b/src/analytics/features/types.ts @@ -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', diff --git a/src/components/RichText.tsx b/src/components/RichText.tsx index 25af851723..4be7aebbc6 100644 --- a/src/components/RichText.tsx +++ b/src/components/RichText.tsx @@ -27,6 +27,7 @@ export type RichTextProps = TextStyleProp & interactiveStyle?: StyleProp 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} ) } @@ -102,6 +106,8 @@ export function RichText({ // @ts-ignore web only -prf dataSet={WORD_WRAP}> {text} + {suffix ? ' ' : null} + {suffix} ) } @@ -188,6 +194,8 @@ export function RichText({ // @ts-ignore web only -prf dataSet={WORD_WRAP}> {els} + {suffix ? ' ' : null} + {suffix} ) } diff --git a/src/screens/PostThread/components/ThreadItemAnchor.tsx b/src/screens/PostThread/components/ThreadItemAnchor.tsx index 7973bc1a8a..0c560caa84 100644 --- a/src/screens/PostThread/components/ThreadItemAnchor.tsx +++ b/src/screens/PostThread/components/ThreadItemAnchor.tsx @@ -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) ? ( + + ) : undefined + } /> - ) : undefined} + ) : ( + + )} {post.embed && ( diff --git a/src/screens/PostThread/components/ThreadItemPost.tsx b/src/screens/PostThread/components/ThreadItemPost.tsx index de0b266526..d254d4be28 100644 --- a/src/screens/PostThread/components/ThreadItemPost.tsx +++ b/src/screens/PostThread/components/ThreadItemPost.tsx @@ -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) ? ( + + ) : undefined + } /> {limitLines && ( - + + + + )} - ) : undefined} + ) : ( + + )} {post.embed && ( = 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 ( + + + + {index}/{count} + + + + ) +} diff --git a/src/screens/PostThread/components/ThreadItemTreePost.tsx b/src/screens/PostThread/components/ThreadItemTreePost.tsx index 83b77b4882..d3dca1960d 100644 --- a/src/screens/PostThread/components/ThreadItemTreePost.tsx +++ b/src/screens/PostThread/components/ThreadItemTreePost.tsx @@ -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) ? ( + + ) : undefined + } /> {limitLines && ( - + + + + )} - ) : null} + ) : ( + + )} {post.embed && (