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 {FALLBACK_MARKER_POST} from './feed/home'
|
||||||
import {type ReasonFeedSource} from './feed/types'
|
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 = (
|
export type FeedTunerFn = (
|
||||||
tuner: FeedTuner,
|
tuner: FeedTuner,
|
||||||
@@ -15,6 +62,7 @@ export type FeedTunerFn = (
|
|||||||
type FeedSliceItem = {
|
type FeedSliceItem = {
|
||||||
post: app.bsky.feed.defs.PostView
|
post: app.bsky.feed.defs.PostView
|
||||||
record: app.bsky.feed.post.Main
|
record: app.bsky.feed.post.Main
|
||||||
|
postNumbering: FeedPostNumbering | undefined
|
||||||
parentAuthor: app.bsky.actor.defs.ProfileViewBasic | undefined
|
parentAuthor: app.bsky.actor.defs.ProfileViewBasic | undefined
|
||||||
isParentBlocked: boolean
|
isParentBlocked: boolean
|
||||||
isParentNotFound: boolean
|
isParentNotFound: boolean
|
||||||
@@ -38,7 +86,10 @@ export class FeedViewPostsSlice {
|
|||||||
rootUri: string
|
rootUri: string
|
||||||
feedPostUri: string
|
feedPostUri: string
|
||||||
|
|
||||||
constructor(feedPost: FeedViewPost) {
|
constructor(
|
||||||
|
feedPost: FeedViewPost,
|
||||||
|
postNumberingByUri: Map<string, FeedPostNumbering>,
|
||||||
|
) {
|
||||||
const {post, reply, reason} = feedPost
|
const {post, reply, reason} = feedPost
|
||||||
this.items = []
|
this.items = []
|
||||||
this.isIncompleteThread = false
|
this.isIncompleteThread = false
|
||||||
@@ -80,6 +131,7 @@ export class FeedViewPostsSlice {
|
|||||||
this.items.push({
|
this.items.push({
|
||||||
post,
|
post,
|
||||||
record: post.record,
|
record: post.record,
|
||||||
|
postNumbering: postNumberingByUri.get(post.uri),
|
||||||
parentAuthor,
|
parentAuthor,
|
||||||
isParentBlocked,
|
isParentBlocked,
|
||||||
isParentNotFound,
|
isParentNotFound,
|
||||||
@@ -128,6 +180,9 @@ export class FeedViewPostsSlice {
|
|||||||
this.items.unshift({
|
this.items.unshift({
|
||||||
post: parent,
|
post: parent,
|
||||||
record: parent.record,
|
record: parent.record,
|
||||||
|
postNumbering:
|
||||||
|
postNumberingByUri.get(parent.uri) ??
|
||||||
|
inferPostNumbering(feedPost, 'parent'),
|
||||||
parentAuthor: grandparentAuthor,
|
parentAuthor: grandparentAuthor,
|
||||||
isParentBlocked: isGrandparentBlocked,
|
isParentBlocked: isGrandparentBlocked,
|
||||||
isParentNotFound: isGrandparentNotFound,
|
isParentNotFound: isGrandparentNotFound,
|
||||||
@@ -151,6 +206,9 @@ export class FeedViewPostsSlice {
|
|||||||
this.items.unshift({
|
this.items.unshift({
|
||||||
post: root,
|
post: root,
|
||||||
record: root.record,
|
record: root.record,
|
||||||
|
postNumbering:
|
||||||
|
postNumberingByUri.get(root.uri) ??
|
||||||
|
inferPostNumbering(feedPost, 'root'),
|
||||||
isParentBlocked: false,
|
isParentBlocked: false,
|
||||||
isParentNotFound: false,
|
isParentNotFound: false,
|
||||||
parentAuthor: undefined,
|
parentAuthor: undefined,
|
||||||
@@ -241,8 +299,16 @@ export class FeedTuner {
|
|||||||
dryRun: false,
|
dryRun: false,
|
||||||
},
|
},
|
||||||
): FeedViewPostsSlice[] {
|
): 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
|
let slices: FeedViewPostsSlice[] = feed
|
||||||
.map(item => new FeedViewPostsSlice(item))
|
.map(item => new FeedViewPostsSlice(item, postNumberingByUri))
|
||||||
.filter(s => s.items.length > 0 || s.isFallbackMarker)
|
.filter(s => s.items.length > 0 || s.isFallbackMarker)
|
||||||
|
|
||||||
// run the custom tuners
|
// run the custom tuners
|
||||||
|
|||||||
@@ -11,12 +11,17 @@ import {type app} from '#/lexicons'
|
|||||||
*/
|
*/
|
||||||
export const POST_NUMBER_INLINE_OFFSET = 6
|
export const POST_NUMBER_INLINE_OFFSET = 6
|
||||||
|
|
||||||
|
export type ThreadItemPostNumbering = Pick<
|
||||||
|
app.bsky.unspecced.defs.ThreadItemPost,
|
||||||
|
'opThreadPostIndex' | 'opThreadPostCount'
|
||||||
|
>
|
||||||
|
|
||||||
export function useHasThreadItemPostNumber(
|
export function useHasThreadItemPostNumber(
|
||||||
value: app.bsky.unspecced.defs.ThreadItemPost,
|
value: ThreadItemPostNumbering | undefined,
|
||||||
) {
|
) {
|
||||||
const ax = useAnalytics()
|
const ax = useAnalytics()
|
||||||
const index = value.opThreadPostIndex
|
const index = value?.opThreadPostIndex
|
||||||
const count = value.opThreadPostCount
|
const count = value?.opThreadPostCount
|
||||||
|
|
||||||
return (
|
return (
|
||||||
ax.features.enabled(ax.features.CanonicalPostNumberingEnable) &&
|
ax.features.enabled(ax.features.CanonicalPostNumberingEnable) &&
|
||||||
@@ -32,14 +37,14 @@ export function ThreadItemPostNumber({
|
|||||||
value,
|
value,
|
||||||
inline = true,
|
inline = true,
|
||||||
}: {
|
}: {
|
||||||
value: app.bsky.unspecced.defs.ThreadItemPost
|
value: ThreadItemPostNumbering | undefined
|
||||||
inline?: boolean
|
inline?: boolean
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {t: l} = useLingui()
|
const {t: l} = useLingui()
|
||||||
const shouldRender = useHasThreadItemPostNumber(value)
|
const shouldRender = useHasThreadItemPostNumber(value)
|
||||||
const index = value.opThreadPostIndex
|
const index = value?.opThreadPostIndex
|
||||||
const count = value.opThreadPostCount
|
const count = value?.opThreadPostCount
|
||||||
|
|
||||||
if (!shouldRender) {
|
if (!shouldRender) {
|
||||||
return null
|
return null
|
||||||
@@ -59,7 +64,8 @@ export function ThreadItemPostNumber({
|
|||||||
},
|
},
|
||||||
inline
|
inline
|
||||||
? platform({
|
? platform({
|
||||||
native: {transform: [{translateY: POST_NUMBER_INLINE_OFFSET}]},
|
android: {transform: [{translateY: POST_NUMBER_INLINE_OFFSET}]},
|
||||||
|
ios: {transform: [{translateY: a.py_2xs.paddingBottom}]},
|
||||||
web: {
|
web: {
|
||||||
top: -2,
|
top: -2,
|
||||||
marginBottom: -2,
|
marginBottom: -2,
|
||||||
|
|||||||
@@ -984,6 +984,7 @@ export function Explore({
|
|||||||
<PostFeedItem
|
<PostFeedItem
|
||||||
post={subItem.post}
|
post={subItem.post}
|
||||||
record={subItem.record}
|
record={subItem.record}
|
||||||
|
postNumbering={subItem.postNumbering}
|
||||||
reason={indexInSlice === 0 ? slice.reason : undefined}
|
reason={indexInSlice === 0 ? slice.reason : undefined}
|
||||||
feedContext={slice.feedContext}
|
feedContext={slice.feedContext}
|
||||||
reqId={slice.reqId}
|
reqId={slice.reqId}
|
||||||
|
|||||||
@@ -233,6 +233,7 @@ export function useFeedPreviews(
|
|||||||
uri: subItem.post.uri,
|
uri: subItem.post.uri,
|
||||||
post: subItem.post,
|
post: subItem.post,
|
||||||
record: subItem.record,
|
record: subItem.record,
|
||||||
|
postNumbering: subItem.postNumbering,
|
||||||
moderation: moderations[i],
|
moderation: moderations[i],
|
||||||
parentAuthor: subItem.parentAuthor,
|
parentAuthor: subItem.parentAuthor,
|
||||||
isParentBlocked: subItem.isParentBlocked,
|
isParentBlocked: subItem.isParentBlocked,
|
||||||
|
|||||||
@@ -25,7 +25,11 @@ import {MergeFeedAPI} from '#/lib/api/feed/merge'
|
|||||||
import {PostListFeedAPI} from '#/lib/api/feed/posts'
|
import {PostListFeedAPI} from '#/lib/api/feed/posts'
|
||||||
import {type FeedAPI, type ReasonFeedSource} from '#/lib/api/feed/types'
|
import {type FeedAPI, type ReasonFeedSource} from '#/lib/api/feed/types'
|
||||||
import {aggregateUserInterests} from '#/lib/api/feed/utils'
|
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 {DISCOVER_FEED_URI} from '#/lib/constants'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {STALE} from '#/state/queries'
|
import {STALE} from '#/state/queries'
|
||||||
@@ -81,6 +85,7 @@ export interface FeedPostSliceItem {
|
|||||||
uri: AtUriString
|
uri: AtUriString
|
||||||
post: app.bsky.feed.defs.PostView
|
post: app.bsky.feed.defs.PostView
|
||||||
record: app.bsky.feed.post.Main
|
record: app.bsky.feed.post.Main
|
||||||
|
postNumbering?: FeedPostNumbering
|
||||||
moderation: ModerationDecision
|
moderation: ModerationDecision
|
||||||
parentAuthor?: app.bsky.actor.defs.ProfileViewBasic
|
parentAuthor?: app.bsky.actor.defs.ProfileViewBasic
|
||||||
isParentBlocked?: boolean
|
isParentBlocked?: boolean
|
||||||
@@ -340,6 +345,7 @@ export function usePostFeedQuery(
|
|||||||
uri: item.post.uri,
|
uri: item.post.uri,
|
||||||
post: item.post,
|
post: item.post,
|
||||||
record: item.record,
|
record: item.record,
|
||||||
|
postNumbering: item.postNumbering,
|
||||||
moderation: moderations[i],
|
moderation: moderations[i],
|
||||||
parentAuthor: item.parentAuthor,
|
parentAuthor: item.parentAuthor,
|
||||||
isParentBlocked: item.isParentBlocked,
|
isParentBlocked: item.isParentBlocked,
|
||||||
|
|||||||
@@ -872,6 +872,7 @@ let PostFeed = ({
|
|||||||
<PostFeedItem
|
<PostFeedItem
|
||||||
post={item.post}
|
post={item.post}
|
||||||
record={item.record}
|
record={item.record}
|
||||||
|
postNumbering={item.postNumbering}
|
||||||
reason={indexInSlice === 0 ? slice.reason : undefined}
|
reason={indexInSlice === 0 ? slice.reason : undefined}
|
||||||
feedContext={slice.feedContext}
|
feedContext={slice.feedContext}
|
||||||
reqId={slice.reqId}
|
reqId={slice.reqId}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {RichText as RichTextAPI} from '@bsky/sdk/richtext'
|
|||||||
import {useQueryClient} from '@tanstack/react-query'
|
import {useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {type ReasonFeedSource} from '#/lib/api/feed/types'
|
import {type ReasonFeedSource} from '#/lib/api/feed/types'
|
||||||
|
import {type FeedPostNumbering} from '#/lib/api/feed-manip'
|
||||||
import {MAX_POST_LINES} from '#/lib/constants'
|
import {MAX_POST_LINES} from '#/lib/constants'
|
||||||
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
|
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
|
||||||
import {usePalette} from '#/lib/hooks/usePalette'
|
import {usePalette} from '#/lib/hooks/usePalette'
|
||||||
@@ -27,6 +28,11 @@ import {
|
|||||||
import {Link} from '#/view/com/util/Link'
|
import {Link} from '#/view/com/util/Link'
|
||||||
import {PostMeta} from '#/view/com/util/PostMeta'
|
import {PostMeta} from '#/view/com/util/PostMeta'
|
||||||
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
|
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 {atoms as a, select, useTheme} from '#/alf'
|
||||||
import {
|
import {
|
||||||
GalleryBleed,
|
GalleryBleed,
|
||||||
@@ -53,6 +59,7 @@ import {PostFeedReason} from './PostFeedReason'
|
|||||||
|
|
||||||
interface FeedItemProps {
|
interface FeedItemProps {
|
||||||
record: app.bsky.feed.post.Main
|
record: app.bsky.feed.post.Main
|
||||||
|
postNumbering?: FeedPostNumbering
|
||||||
reason:
|
reason:
|
||||||
| app.bsky.feed.defs.ReasonRepost
|
| app.bsky.feed.defs.ReasonRepost
|
||||||
| app.bsky.feed.defs.ReasonPin
|
| app.bsky.feed.defs.ReasonPin
|
||||||
@@ -75,6 +82,7 @@ interface FeedItemProps {
|
|||||||
export function PostFeedItem({
|
export function PostFeedItem({
|
||||||
post,
|
post,
|
||||||
record,
|
record,
|
||||||
|
postNumbering,
|
||||||
reason,
|
reason,
|
||||||
feedContext,
|
feedContext,
|
||||||
reqId,
|
reqId,
|
||||||
@@ -112,6 +120,7 @@ export function PostFeedItem({
|
|||||||
<FeedItemInner
|
<FeedItemInner
|
||||||
post={postShadowed}
|
post={postShadowed}
|
||||||
record={record}
|
record={record}
|
||||||
|
postNumbering={postNumbering}
|
||||||
reason={reason}
|
reason={reason}
|
||||||
feedContext={feedContext}
|
feedContext={feedContext}
|
||||||
reqId={reqId}
|
reqId={reqId}
|
||||||
@@ -137,6 +146,7 @@ export function PostFeedItem({
|
|||||||
let FeedItemInner = ({
|
let FeedItemInner = ({
|
||||||
post,
|
post,
|
||||||
record,
|
record,
|
||||||
|
postNumbering,
|
||||||
reason,
|
reason,
|
||||||
feedContext,
|
feedContext,
|
||||||
reqId,
|
reqId,
|
||||||
@@ -426,6 +436,7 @@ let FeedItemInner = ({
|
|||||||
<PostContent
|
<PostContent
|
||||||
moderation={moderation}
|
moderation={moderation}
|
||||||
richText={richText}
|
richText={richText}
|
||||||
|
postNumbering={postNumbering}
|
||||||
postEmbed={post.embed}
|
postEmbed={post.embed}
|
||||||
postAuthor={post.author}
|
postAuthor={post.author}
|
||||||
onOpenEmbed={onOpenEmbed}
|
onOpenEmbed={onOpenEmbed}
|
||||||
@@ -457,6 +468,7 @@ FeedItemInner = memo(FeedItemInner)
|
|||||||
|
|
||||||
let PostContent = ({
|
let PostContent = ({
|
||||||
post,
|
post,
|
||||||
|
postNumbering,
|
||||||
moderation,
|
moderation,
|
||||||
richText,
|
richText,
|
||||||
postEmbed,
|
postEmbed,
|
||||||
@@ -471,12 +483,14 @@ let PostContent = ({
|
|||||||
postAuthor: app.bsky.feed.defs.PostView['author']
|
postAuthor: app.bsky.feed.defs.PostView['author']
|
||||||
onOpenEmbed: () => void
|
onOpenEmbed: () => void
|
||||||
post: app.bsky.feed.defs.PostView
|
post: app.bsky.feed.defs.PostView
|
||||||
|
postNumbering: FeedPostNumbering | undefined
|
||||||
additionalPostAlerts?: AppModerationCause[]
|
additionalPostAlerts?: AppModerationCause[]
|
||||||
feedDescriptor?: string
|
feedDescriptor?: string
|
||||||
}): React.ReactNode => {
|
}): React.ReactNode => {
|
||||||
const [limitLines, setLimitLines] = useState(
|
const [limitLines, setLimitLines] = useState(
|
||||||
() => countLines(richText.text) >= MAX_POST_LINES,
|
() => countLines(richText.text) >= MAX_POST_LINES,
|
||||||
)
|
)
|
||||||
|
const showPostNumber = useHasThreadItemPostNumber(postNumbering)
|
||||||
|
|
||||||
const record = useMemo<app.bsky.feed.post.Main | undefined>(
|
const record = useMemo<app.bsky.feed.post.Main | undefined>(
|
||||||
() =>
|
() =>
|
||||||
@@ -510,12 +524,26 @@ let PostContent = ({
|
|||||||
style={[a.flex_1, a.text_md]}
|
style={[a.flex_1, a.text_md]}
|
||||||
authorHandle={postAuthor.handle}
|
authorHandle={postAuthor.handle}
|
||||||
shouldProxyLinks={true}
|
shouldProxyLinks={true}
|
||||||
|
suffixOffset={POST_NUMBER_INLINE_OFFSET}
|
||||||
|
suffix={
|
||||||
|
!limitLines && showPostNumber ? (
|
||||||
|
<ThreadItemPostNumber value={postNumbering} />
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
{limitLines && (
|
{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>
|
</View>
|
||||||
) : undefined}
|
) : (
|
||||||
|
<ThreadItemPostNumber inline={false} value={postNumbering} />
|
||||||
|
)}
|
||||||
{record && <TranslatedPost hideTranslateLink post={post} />}
|
{record && <TranslatedPost hideTranslateLink post={post} />}
|
||||||
{postEmbed ? (
|
{postEmbed ? (
|
||||||
<View
|
<View
|
||||||
|
|||||||
Reference in New Issue
Block a user