Add OP thread numbering to feed posts (#11472)
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user