diff --git a/src/lib/api/feed-manip.ts b/src/lib/api/feed-manip.ts index cd99fc9d57..b91613f4e7 100644 --- a/src/lib/api/feed-manip.ts +++ b/src/lib/api/feed-manip.ts @@ -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 + +// 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, + ) { 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() + 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 diff --git a/src/screens/PostThread/components/ThreadItemPostNumber.tsx b/src/screens/PostThread/components/ThreadItemPostNumber.tsx index 04db513a2b..c07aa98e5d 100644 --- a/src/screens/PostThread/components/ThreadItemPostNumber.tsx +++ b/src/screens/PostThread/components/ThreadItemPostNumber.tsx @@ -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, diff --git a/src/screens/Search/Explore.tsx b/src/screens/Search/Explore.tsx index 87e2301e1d..210c19265a 100644 --- a/src/screens/Search/Explore.tsx +++ b/src/screens/Search/Explore.tsx @@ -984,6 +984,7 @@ export function Explore({ 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( () => @@ -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 ? ( + + ) : undefined + } /> {limitLines && ( - + + + + )} - ) : undefined} + ) : ( + + )} {record && } {postEmbed ? (