Preserve thread numbering in placeholders (#11616)
This commit is contained in:
@@ -11,7 +11,11 @@ import {
|
||||
|
||||
import {CustomFeedAPI} from '#/lib/api/feed/custom'
|
||||
import {aggregateUserInterests} from '#/lib/api/feed/utils'
|
||||
import {FeedTuner} from '#/lib/api/feed-manip'
|
||||
import {
|
||||
createFeedViewPostsSlices,
|
||||
FeedTuner,
|
||||
type ValidFeedPostNumbering,
|
||||
} from '#/lib/api/feed-manip'
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {
|
||||
@@ -403,6 +407,35 @@ export function* findAllPostsInQueryData(
|
||||
}
|
||||
}
|
||||
|
||||
export function findPostNumberingInQueryData(
|
||||
queryClient: QueryClient,
|
||||
uri: string,
|
||||
): ValidFeedPostNumbering | undefined {
|
||||
const atUri = new AtUri(uri)
|
||||
const queryDatas = queryClient.getQueriesData<
|
||||
InfiniteData<{
|
||||
feed: app.bsky.feed.defs.GeneratorView
|
||||
posts: app.bsky.feed.defs.FeedViewPost[]
|
||||
}>
|
||||
>({
|
||||
queryKey: [RQKEY_ROOT],
|
||||
})
|
||||
|
||||
for (const [_queryKey, queryData] of queryDatas) {
|
||||
if (!queryData?.pages) continue
|
||||
|
||||
for (const page of queryData.pages) {
|
||||
for (const slice of createFeedViewPostsSlices(page.posts)) {
|
||||
for (const item of slice.items) {
|
||||
if (item.postNumbering && didOrHandleUriMatches(atUri, item.post)) {
|
||||
return item.postNumbering
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function* findAllProfilesInQueryData(
|
||||
queryClient: QueryClient,
|
||||
did: string,
|
||||
|
||||
@@ -26,9 +26,11 @@ 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 {
|
||||
createFeedViewPostsSlices,
|
||||
type FeedPostNumbering,
|
||||
FeedTuner,
|
||||
type FeedTunerFn,
|
||||
type ValidFeedPostNumbering,
|
||||
} from '#/lib/api/feed-manip'
|
||||
import {DISCOVER_FEED_URI} from '#/lib/constants'
|
||||
import {logger} from '#/logger'
|
||||
@@ -527,6 +529,32 @@ export function* findAllPostsInQueryData(
|
||||
}
|
||||
}
|
||||
|
||||
export function findPostNumberingInQueryData(
|
||||
queryClient: QueryClient,
|
||||
uri: string,
|
||||
): ValidFeedPostNumbering | undefined {
|
||||
const atUri = new AtUri(uri)
|
||||
const queryDatas = queryClient.getQueriesData<
|
||||
InfiniteData<FeedPageUnselected>
|
||||
>({
|
||||
queryKey: [RQKEY_ROOT],
|
||||
})
|
||||
|
||||
for (const [_queryKey, queryData] of queryDatas) {
|
||||
if (!queryData?.pages) continue
|
||||
|
||||
for (const page of queryData.pages) {
|
||||
for (const slice of createFeedViewPostsSlices(page.feed)) {
|
||||
for (const item of slice.items) {
|
||||
if (item.postNumbering && didOrHandleUriMatches(atUri, item.post)) {
|
||||
return item.postNumbering
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function* findAllProfilesInQueryData(
|
||||
queryClient: QueryClient,
|
||||
did: string,
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import {QueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {findAllPostsInQueryData as findAllPostsInBookmarksQueryData} from '#/state/queries/bookmarks/useBookmarksQuery'
|
||||
import {
|
||||
findAllPostsInQueryData as findAllPostsInExploreFeedPreviewsQueryData,
|
||||
findPostNumberingInQueryData as findPostNumberingInExploreFeedPreviewsQueryData,
|
||||
} from '#/state/queries/explore-feed-previews'
|
||||
import {findAllPostsInQueryData as findAllPostsInNotifsQueryData} from '#/state/queries/notifications/feed'
|
||||
import {
|
||||
findAllPostsInQueryData as findAllPostsInFeedQueryData,
|
||||
findPostNumberingInQueryData as findPostNumberingInFeedQueryData,
|
||||
} from '#/state/queries/post-feed'
|
||||
import {findAllPostsInQueryData as findAllPostsInQuoteQueryData} from '#/state/queries/post-quotes'
|
||||
import {findAllPostsInQueryData as findAllPostsInSearchQueryData} from '#/state/queries/search-posts-v2'
|
||||
import {type app} from '#/lexicons'
|
||||
import {getThreadPlaceholder} from './queryCache'
|
||||
|
||||
jest.mock('#/state/cache/post-shadow', () => ({
|
||||
dangerousGetPostShadow: jest.fn(),
|
||||
updatePostShadow: jest.fn(),
|
||||
}))
|
||||
jest.mock('#/state/queries/bookmarks/useBookmarksQuery', () => ({
|
||||
findAllPostsInQueryData: jest.fn(),
|
||||
}))
|
||||
jest.mock('#/state/queries/explore-feed-previews', () => ({
|
||||
findAllPostsInQueryData: jest.fn(),
|
||||
findPostNumberingInQueryData: jest.fn(),
|
||||
}))
|
||||
jest.mock('#/state/queries/notifications/feed', () => ({
|
||||
findAllPostsInQueryData: jest.fn(),
|
||||
}))
|
||||
jest.mock('#/state/queries/post-feed', () => ({
|
||||
findAllPostsInQueryData: jest.fn(),
|
||||
findPostNumberingInQueryData: jest.fn(),
|
||||
}))
|
||||
jest.mock('#/state/queries/post-quotes', () => ({
|
||||
findAllPostsInQueryData: jest.fn(),
|
||||
}))
|
||||
jest.mock('#/state/queries/search-posts-v2', () => ({
|
||||
findAllPostsInQueryData: jest.fn(),
|
||||
}))
|
||||
jest.mock('#/state/queries/usePostThread', () => ({
|
||||
usePostThreadContext: jest.fn(),
|
||||
}))
|
||||
|
||||
const finders = [
|
||||
findAllPostsInBookmarksQueryData,
|
||||
findAllPostsInExploreFeedPreviewsQueryData,
|
||||
findAllPostsInNotifsQueryData,
|
||||
findAllPostsInFeedQueryData,
|
||||
findAllPostsInQuoteQueryData,
|
||||
findAllPostsInSearchQueryData,
|
||||
]
|
||||
|
||||
function post(uri: string, likeCount: number) {
|
||||
return {
|
||||
$type: 'app.bsky.feed.defs#postView',
|
||||
uri,
|
||||
likeCount,
|
||||
} as app.bsky.feed.defs.PostView
|
||||
}
|
||||
|
||||
describe('getThreadPlaceholder', () => {
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks()
|
||||
for (const finder of finders) {
|
||||
jest.mocked(finder).mockImplementation(function* () {})
|
||||
}
|
||||
jest.mocked(findPostNumberingInFeedQueryData).mockReturnValue(undefined)
|
||||
jest
|
||||
.mocked(findPostNumberingInExploreFeedPreviewsQueryData)
|
||||
.mockReturnValue(undefined)
|
||||
})
|
||||
|
||||
it('combines feed numbering with the preferred cached post', () => {
|
||||
const uri = 'at://did:plc:alice/app.bsky.feed.post/1'
|
||||
const notificationPost = post(uri, 4)
|
||||
const feedPost = post(uri, 1)
|
||||
jest.mocked(findPostNumberingInFeedQueryData).mockReturnValue({
|
||||
opThreadPostIndex: 2,
|
||||
opThreadPostCount: 4,
|
||||
})
|
||||
jest.mocked(findAllPostsInNotifsQueryData).mockImplementation(function* () {
|
||||
yield notificationPost
|
||||
return undefined
|
||||
})
|
||||
jest.mocked(findAllPostsInFeedQueryData).mockImplementation(function* () {
|
||||
yield feedPost
|
||||
return undefined
|
||||
})
|
||||
|
||||
const placeholder = getThreadPlaceholder(queryClient, uri)
|
||||
|
||||
expect(placeholder?.value).toMatchObject({
|
||||
post: notificationPost,
|
||||
opThread: true,
|
||||
opThreadPostIndex: 2,
|
||||
opThreadPostCount: 4,
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps non-numbered placeholders out of the OP thread', () => {
|
||||
const uri = 'at://did:plc:alice/app.bsky.feed.post/1'
|
||||
jest.mocked(findAllPostsInFeedQueryData).mockImplementation(function* () {
|
||||
yield post(uri, 1)
|
||||
return undefined
|
||||
})
|
||||
|
||||
const placeholder = getThreadPlaceholder(queryClient, uri)
|
||||
|
||||
expect(placeholder?.value).toMatchObject({opThread: false})
|
||||
expect(placeholder?.value).not.toHaveProperty('opThreadPostIndex')
|
||||
expect(placeholder?.value).not.toHaveProperty('opThreadPostCount')
|
||||
})
|
||||
})
|
||||
@@ -3,14 +3,21 @@ import {type $Typed} from '@atproto/lex'
|
||||
import {AtUri} from '@atproto/syntax'
|
||||
import {type QueryClient, useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {type ValidFeedPostNumbering} from '#/lib/api/feed-manip'
|
||||
import {
|
||||
dangerousGetPostShadow,
|
||||
updatePostShadow,
|
||||
} from '#/state/cache/post-shadow'
|
||||
import {findAllPostsInQueryData as findAllPostsInBookmarksQueryData} from '#/state/queries/bookmarks/useBookmarksQuery'
|
||||
import {findAllPostsInQueryData as findAllPostsInExploreFeedPreviewsQueryData} from '#/state/queries/explore-feed-previews'
|
||||
import {
|
||||
findAllPostsInQueryData as findAllPostsInExploreFeedPreviewsQueryData,
|
||||
findPostNumberingInQueryData as findPostNumberingInExploreFeedPreviewsQueryData,
|
||||
} from '#/state/queries/explore-feed-previews'
|
||||
import {findAllPostsInQueryData as findAllPostsInNotifsQueryData} from '#/state/queries/notifications/feed'
|
||||
import {findAllPostsInQueryData as findAllPostsInFeedQueryData} from '#/state/queries/post-feed'
|
||||
import {
|
||||
findAllPostsInQueryData as findAllPostsInFeedQueryData,
|
||||
findPostNumberingInQueryData as findPostNumberingInFeedQueryData,
|
||||
} from '#/state/queries/post-feed'
|
||||
import {findAllPostsInQueryData as findAllPostsInQuoteQueryData} from '#/state/queries/post-quotes'
|
||||
import {findAllPostsInQueryData as findAllPostsInSearchQueryData} from '#/state/queries/search-posts-v2'
|
||||
import {usePostThreadContext} from '#/state/queries/usePostThread'
|
||||
@@ -207,8 +214,15 @@ export function getThreadPlaceholder(
|
||||
queryClient: QueryClient,
|
||||
uri: string,
|
||||
): $Typed<app.bsky.unspecced.getPostThreadV2.ThreadItem> | void {
|
||||
const postNumbering =
|
||||
findPostNumberingInFeedQueryData(queryClient, uri) ??
|
||||
findPostNumberingInExploreFeedPreviewsQueryData(queryClient, uri)
|
||||
let partial
|
||||
for (let item of getThreadPlaceholderCandidates(queryClient, uri)) {
|
||||
for (let item of getThreadPlaceholderCandidates(
|
||||
queryClient,
|
||||
uri,
|
||||
postNumbering,
|
||||
)) {
|
||||
/*
|
||||
* Currently, the backend doesn't send full post info in some cases (for
|
||||
* example, for quoted posts). We use missing `likeCount` as a way to
|
||||
@@ -231,6 +245,7 @@ export function getThreadPlaceholder(
|
||||
export function* getThreadPlaceholderCandidates(
|
||||
queryClient: QueryClient,
|
||||
uri: string,
|
||||
postNumbering?: ValidFeedPostNumbering,
|
||||
): Generator<
|
||||
$Typed<
|
||||
Omit<app.bsky.unspecced.getPostThreadV2.ThreadItem, 'value'> & {
|
||||
@@ -243,7 +258,7 @@ export function* getThreadPlaceholderCandidates(
|
||||
* Check post thread queries first
|
||||
*/
|
||||
for (const post of findAllPostsInQueryData(queryClient, uri)) {
|
||||
yield postViewToThreadPlaceholder(post)
|
||||
yield postViewToThreadPlaceholder(post, postNumbering)
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -253,25 +268,25 @@ export function* getThreadPlaceholderCandidates(
|
||||
* avoid a notification->post scroll jump.
|
||||
*/
|
||||
for (let post of findAllPostsInNotifsQueryData(queryClient, uri)) {
|
||||
yield postViewToThreadPlaceholder(post)
|
||||
yield postViewToThreadPlaceholder(post, postNumbering)
|
||||
}
|
||||
for (let post of findAllPostsInFeedQueryData(queryClient, uri)) {
|
||||
yield postViewToThreadPlaceholder(post)
|
||||
yield postViewToThreadPlaceholder(post, postNumbering)
|
||||
}
|
||||
for (let post of findAllPostsInQuoteQueryData(queryClient, uri)) {
|
||||
yield postViewToThreadPlaceholder(post)
|
||||
yield postViewToThreadPlaceholder(post, postNumbering)
|
||||
}
|
||||
for (let post of findAllPostsInSearchQueryData(queryClient, uri)) {
|
||||
yield postViewToThreadPlaceholder(post)
|
||||
yield postViewToThreadPlaceholder(post, postNumbering)
|
||||
}
|
||||
for (let post of findAllPostsInBookmarksQueryData(queryClient, uri)) {
|
||||
yield postViewToThreadPlaceholder(post)
|
||||
yield postViewToThreadPlaceholder(post, postNumbering)
|
||||
}
|
||||
for (let post of findAllPostsInExploreFeedPreviewsQueryData(
|
||||
queryClient,
|
||||
uri,
|
||||
)) {
|
||||
yield postViewToThreadPlaceholder(post)
|
||||
yield postViewToThreadPlaceholder(post, postNumbering)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import {type $Typed} from '@atproto/lex'
|
||||
import {AtUri} from '@atproto/syntax'
|
||||
import {moderatePost, type ModerationOpts} from '@bsky/sdk/moderation'
|
||||
|
||||
import {type ValidFeedPostNumbering} from '#/lib/api/feed-manip'
|
||||
import {makeProfileLink} from '#/lib/routes/links'
|
||||
import {
|
||||
type ApiThreadItem,
|
||||
@@ -156,6 +157,7 @@ export function skeleton({
|
||||
|
||||
export function postViewToThreadPlaceholder(
|
||||
post: app.bsky.feed.defs.PostView,
|
||||
postNumbering?: ValidFeedPostNumbering,
|
||||
): $Typed<
|
||||
Omit<app.bsky.unspecced.getPostThreadV2.ThreadItem, 'value'> & {
|
||||
value: $Typed<app.bsky.unspecced.defs.ThreadItemPost>
|
||||
@@ -168,7 +170,8 @@ export function postViewToThreadPlaceholder(
|
||||
value: {
|
||||
$type: 'app.bsky.unspecced.defs#threadItemPost',
|
||||
post,
|
||||
opThread: false,
|
||||
opThread: !!postNumbering,
|
||||
...postNumbering,
|
||||
moreParents: false,
|
||||
moreReplies: 0,
|
||||
hiddenByThreadgate: false,
|
||||
|
||||
Reference in New Issue
Block a user