Update types

This commit is contained in:
Eric Bailey
2025-05-19 18:17:21 -05:00
parent 03b5ef162c
commit 8e2bdecc25
7 changed files with 283 additions and 192 deletions
+10 -7
View File
@@ -1,8 +1,11 @@
import {useQuery, useQueryClient} from '@tanstack/react-query' import {useQuery, useQueryClient} from '@tanstack/react-query'
import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {getThreadPlaceholder, createCacheMutator} from '#/state/queries/usePostThread/queryCache' import {
import {flatten,sort} from '#/state/queries/usePostThread/traversal' createCacheMutator,
getThreadPlaceholder,
} from '#/state/queries/usePostThread/queryCache'
import {flatten, sort} from '#/state/queries/usePostThread/traversal'
import { import {
createPostThreadQueryKey, createPostThreadQueryKey,
HiddenReplyKind, HiddenReplyKind,
@@ -31,9 +34,9 @@ export function usePostThread({
const enabled = isEnabled !== false && !!uri && !!moderationOpts const enabled = isEnabled !== false && !!uri && !!moderationOpts
const queryKey = createPostThreadQueryKey({ const queryKey = createPostThreadQueryKey({
uri, uri,
params, params,
}) })
const query = useQuery({ const query = useQuery({
enabled, enabled,
@@ -41,7 +44,7 @@ export function usePostThread({
async queryFn() { async queryFn() {
const {data} = await agent.app.bsky.unspecced.getPostThreadV2({ const {data} = await agent.app.bsky.unspecced.getPostThreadV2({
uri: uri!, uri: uri!,
branchingFactor: params.view === 'linear' ? 1 : 10, nestedBranchingFactor: params.view === 'linear' ? 1 : 10,
below: 10, below: 10,
sorting: mapSortOptionsToSortID(params.sort), sorting: mapSortOptionsToSortID(params.sort),
}) })
@@ -91,7 +94,7 @@ export function usePostThread({
return { return {
...query, ...query,
data: { data: {
slices: items, items,
threadgate: query.data?.threadgate, threadgate: query.data?.threadgate,
}, },
insertReplies: mutator.insertReplies, insertReplies: mutator.insertReplies,
+38 -24
View File
@@ -1,9 +1,4 @@
import { import {type $Typed, AppBskyUnspeccedGetPostThreadV2, AtUri} from '@atproto/api'
type $Typed,
AppBskyUnspeccedDefs,
type AppBskyUnspeccedGetPostThreadV2,
AtUri,
} from '@atproto/api'
import {type QueryClient} from '@tanstack/react-query' import {type QueryClient} from '@tanstack/react-query'
import {findAllPostsInQueryData as findAllPostsInExploreFeedPreviewsQueryData} from '#/state/queries/explore-feed-previews' import {findAllPostsInQueryData as findAllPostsInExploreFeedPreviewsQueryData} from '#/state/queries/explore-feed-previews'
@@ -12,8 +7,8 @@ import {findAllPostsInQueryData as findAllPostsInFeedQueryData} from '#/state/qu
import {findAllPostsInQueryData as findAllPostsInQuoteQueryData} from '#/state/queries/post-quotes' import {findAllPostsInQueryData as findAllPostsInQuoteQueryData} from '#/state/queries/post-quotes'
import {findAllPostsInQueryData as findAllPostsInSearchQueryData} from '#/state/queries/search-posts' import {findAllPostsInQueryData as findAllPostsInSearchQueryData} from '#/state/queries/search-posts'
import { import {
type createPostThreadQueryKey,
postThreadQueryKeyRoot, postThreadQueryKeyRoot,
createPostThreadQueryKey,
} from '#/state/queries/usePostThread/types' } from '#/state/queries/usePostThread/types'
import { import {
embedViewToThreadPlaceholder, embedViewToThreadPlaceholder,
@@ -30,8 +25,8 @@ export function createCacheMutator({
}) { }) {
return { return {
insertReplies( insertReplies(
parent: AppBskyUnspeccedDefs.ThreadItemPost, parent: AppBskyUnspeccedGetPostThreadV2.ThreadItem,
replies: AppBskyUnspeccedDefs.ThreadItemPost[], replies: AppBskyUnspeccedGetPostThreadV2.ThreadItem[],
) { ) {
queryClient.setQueryData<AppBskyUnspeccedGetPostThreadV2.OutputSchema>( queryClient.setQueryData<AppBskyUnspeccedGetPostThreadV2.OutputSchema>(
queryKey, queryKey,
@@ -41,16 +36,23 @@ export function createCacheMutator({
const thread = [...queryData.thread] const thread = [...queryData.thread]
for (let i = 0; i < thread.length; i++) { for (let i = 0; i < thread.length; i++) {
const anchor = thread[i] const existingParent = thread[i]
if (!AppBskyUnspeccedDefs.isThreadItemPost(anchor)) continue if (
if (anchor.uri !== parent.uri) continue !AppBskyUnspeccedGetPostThreadV2.isThreadItemPost(
existingParent.value,
)
)
continue
if (!AppBskyUnspeccedGetPostThreadV2.isThreadItemPost(parent.value))
continue
if (existingParent.uri !== parent.uri) continue
/* /*
* Update parent data * Update parent data
*/ */
anchor.post = { existingParent.value.post = {
...anchor.post, ...existingParent.value.post,
replyCount: parent.post.replyCount, replyCount: parent.value.post.replyCount,
} }
/* /*
@@ -58,11 +60,14 @@ export function createCacheMutator({
*/ */
for (let ri = 0; ri < replies.length; ri++) { for (let ri = 0; ri < replies.length; ri++) {
const reply = replies[ri] const reply = replies[ri]
reply.depth = anchor.depth + 1 + ri if (
!AppBskyUnspeccedGetPostThreadV2.isThreadItemPost(reply.value)
)
continue
const insertIndex = i + 1 + ri const insertIndex = i + 1 + ri
thread.splice(insertIndex, 0, { thread.splice(insertIndex, 0, {
$type: 'app.bsky.unspecced.defs#threadItemPost',
...reply, ...reply,
depth: existingParent.depth + 1 + ri,
}) })
} }
} }
@@ -74,14 +79,14 @@ export function createCacheMutator({
}, },
) )
}, },
deletePost(post: AppBskyUnspeccedDefs.ThreadItemPost) {}, deletePost(_post: AppBskyUnspeccedGetPostThreadV2.ThreadItem) {},
} }
} }
export function getThreadPlaceholder( export function getThreadPlaceholder(
queryClient: QueryClient, queryClient: QueryClient,
uri: string, uri: string,
): $Typed<AppBskyUnspeccedDefs.ThreadItemPost> | void { ): $Typed<AppBskyUnspeccedGetPostThreadV2.ThreadItem> | void {
let partial let partial
for (let item of getThreadPlaceholderCandidates(queryClient, uri)) { for (let item of getThreadPlaceholderCandidates(queryClient, uri)) {
/* /*
@@ -92,7 +97,7 @@ export function getThreadPlaceholder(
* *
* TODO can we send in feeds and quotes? * TODO can we send in feeds and quotes?
*/ */
const hasAllInfo = item.post.likeCount != null const hasAllInfo = item.value.post.likeCount != null
if (hasAllInfo) { if (hasAllInfo) {
return item return item
} else { } else {
@@ -106,7 +111,14 @@ export function getThreadPlaceholder(
export function* getThreadPlaceholderCandidates( export function* getThreadPlaceholderCandidates(
queryClient: QueryClient, queryClient: QueryClient,
uri: string, uri: string,
): Generator<$Typed<AppBskyUnspeccedDefs.ThreadItemPost>, void> { ): Generator<
$Typed<
Omit<AppBskyUnspeccedGetPostThreadV2.ThreadItem, 'value'> & {
value: $Typed<AppBskyUnspeccedGetPostThreadV2.ThreadItemPost>
}
>,
void
> {
const atUri = new AtUri(uri) const atUri = new AtUri(uri)
/* /*
@@ -123,15 +135,17 @@ export function* getThreadPlaceholderCandidates(
const {thread} = queryData const {thread} = queryData
for (const item of thread) { for (const item of thread) {
if (AppBskyUnspeccedDefs.isThreadItemPost(item)) { if (AppBskyUnspeccedGetPostThreadV2.isThreadItemPost(item.value)) {
if (didOrHandleUriMatches(atUri, item.post)) { if (didOrHandleUriMatches(atUri, item.value.post)) {
yield { yield {
$type: 'app.bsky.unspecced.getPostThreadV2#threadItem',
...item, ...item,
depth: 0, depth: 0,
value: item.value,
} }
} }
const qp = getEmbeddedPost(item.post.embed) const qp = getEmbeddedPost(item.value.post.embed)
if (qp && didOrHandleUriMatches(atUri, qp)) { if (qp && didOrHandleUriMatches(atUri, qp)) {
yield embedViewToThreadPlaceholder(qp) yield embedViewToThreadPlaceholder(qp)
} }
+68 -44
View File
@@ -1,6 +1,5 @@
import { import {
AppBskyUnspeccedDefs, AppBskyUnspeccedGetPostThreadV2,
type AppBskyUnspeccedGetPostThreadV2,
type ModerationDecision, type ModerationDecision,
type ModerationOpts, type ModerationOpts,
} from '@atproto/api' } from '@atproto/api'
@@ -20,7 +19,7 @@ export function flatten(
showHidden: boolean showHidden: boolean
}, },
) { ) {
const flattened: Slice[] = sorted.slices const flattened: Slice[] = sorted.items
if (sorted.hidden.length) { if (sorted.hidden.length) {
if (showHidden) { if (showHidden) {
@@ -58,8 +57,8 @@ export function flatten(
if (hasSession) { if (hasSession) {
for (let i = 0; i < flattened.length; i++) { for (let i = 0; i < flattened.length; i++) {
const slice = flattened[i] const item = flattened[i]
if ('slice' in slice && slice.slice.depth === 0) { if ('depth' in item && item.depth === 0) {
flattened.splice(i + 1, 0, { flattened.splice(i + 1, 0, {
type: 'replyComposer', type: 'replyComposer',
key: 'replyComposer', key: 'replyComposer',
@@ -82,32 +81,39 @@ export function sort(
moderationOpts: ModerationOpts moderationOpts: ModerationOpts
}, },
) { ) {
const slices: Slice[] = [] const items: Slice[] = []
const hidden: Slice[] = [] const hidden: Slice[] = []
const muted: Slice[] = [] const muted: Slice[] = []
traversal: for (let i = 0; i < thread.length; i++) { traversal: for (let i = 0; i < thread.length; i++) {
const item = thread[i] const item = thread[i]
// ignore unknowns
if (!('depth' in item)) continue
if (item.depth < 0) { if (item.depth < 0) {
/* /*
* Parents are ignored until we find the highlighted post, then we walk * Parents are ignored until we find the highlighted post, then we walk
* _up_ from there. * _up_ from there.
*/ */
} else if (item.depth === 0) { } else if (item.depth === 0) {
if (AppBskyUnspeccedDefs.isThreadItemNoUnauthenticated(item)) { if (
slices.push(views.noUnauthenticated({item})) AppBskyUnspeccedGetPostThreadV2.isThreadItemNoUnauthenticated(
} else if (AppBskyUnspeccedDefs.isThreadItemNotFound(item)) { item.value,
slices.push(views.notFound({item})) )
} else if (AppBskyUnspeccedDefs.isThreadItemBlocked(item)) { ) {
slices.push(views.blocked({item})) items.push(views.threadPostNoUnauthenticated(item))
} else if (AppBskyUnspeccedDefs.isThreadItemPost(item)) { } else if (
slices.push( AppBskyUnspeccedGetPostThreadV2.isThreadItemNotFound(item.value)
views.post({ ) {
item, items.push(views.threadPostNotFound(item))
} else if (
AppBskyUnspeccedGetPostThreadV2.isThreadItemBlocked(item.value)
) {
items.push(views.threadPostBlocked(item))
} else if (AppBskyUnspeccedGetPostThreadV2.isThreadItemPost(item.value)) {
items.push(
views.threadPost({
uri: item.uri,
depth: item.depth,
value: item.value,
oneUp: thread[i - 1], oneUp: thread[i - 1],
oneDown: thread[i + 1], oneDown: thread[i + 1],
moderationOpts, moderationOpts,
@@ -119,19 +125,31 @@ export function sort(
const parent = thread[pi] const parent = thread[pi]
const parentOneUp = thread[pi - 1] const parentOneUp = thread[pi - 1]
if (AppBskyUnspeccedDefs.isThreadItemNoUnauthenticated(parent)) { if (
slices.unshift(views.noUnauthenticated({item: parent})) AppBskyUnspeccedGetPostThreadV2.isThreadItemNoUnauthenticated(
parent.value,
)
) {
items.unshift(views.threadPostNoUnauthenticated(parent))
break parentTraversal break parentTraversal
} else if (AppBskyUnspeccedDefs.isThreadItemNotFound(parent)) { } else if (
slices.unshift(views.notFound({item: parent})) AppBskyUnspeccedGetPostThreadV2.isThreadItemNotFound(parent.value)
) {
items.unshift(views.threadPostNotFound(parent))
break parentTraversal break parentTraversal
} else if (AppBskyUnspeccedDefs.isThreadItemBlocked(parent)) { } else if (
slices.unshift(views.blocked({item: parent})) AppBskyUnspeccedGetPostThreadV2.isThreadItemBlocked(parent.value)
) {
items.unshift(views.threadPostBlocked(parent))
break parentTraversal break parentTraversal
} else if (AppBskyUnspeccedDefs.isThreadItemPost(parent)) { } else if (
slices.unshift( AppBskyUnspeccedGetPostThreadV2.isThreadItemPost(parent.value)
views.post({ ) {
item: parent, items.unshift(
views.threadPost({
uri: parent.uri,
depth: parent.depth,
value: parent.value,
oneUp: parentOneUp, oneUp: parentOneUp,
oneDown: parentOneDown, oneDown: parentOneDown,
moderationOpts, moderationOpts,
@@ -147,22 +165,26 @@ export function sort(
* we could. * we could.
*/ */
const shouldBreak = const shouldBreak =
AppBskyUnspeccedDefs.isThreadItemNoUnauthenticated(item) || AppBskyUnspeccedGetPostThreadV2.isThreadItemNoUnauthenticated(
AppBskyUnspeccedDefs.isThreadItemNotFound(item) || item.value,
AppBskyUnspeccedDefs.isThreadItemBlocked(item) ) ||
AppBskyUnspeccedGetPostThreadV2.isThreadItemNotFound(item.value) ||
AppBskyUnspeccedGetPostThreadV2.isThreadItemBlocked(item.value)
if (shouldBreak) { if (shouldBreak) {
const branch = getBranch(thread, i, item.depth) const branch = getBranch(thread, i, item.depth)
// could insert tombstone // could insert tombstone
i = branch.end i = branch.end
continue traversal continue traversal
} else if (AppBskyUnspeccedDefs.isThreadItemPost(item)) { } else if (AppBskyUnspeccedGetPostThreadV2.isThreadItemPost(item.value)) {
const lastSlice = slices[slices.length - 1] const lastSlice = items[items.length - 1]
const isFirstReply = const isFirstReply =
lastSlice.type === 'replyComposer' || lastSlice.type === 'replyComposer' ||
(lastSlice.type === 'threadSlice' && lastSlice.slice.depth === 0) (lastSlice.type === 'threadPost' && lastSlice.depth === 0)
const parent = views.post({ const parent = views.threadPost({
item, uri: item.uri,
depth: item.depth,
value: item.value,
oneUp: isFirstReply ? undefined : thread[i - 1], oneUp: isFirstReply ? undefined : thread[i - 1],
oneDown: thread[i + 1], oneDown: thread[i + 1],
moderationOpts, moderationOpts,
@@ -177,7 +199,7 @@ export function sort(
/* /*
* Not hidden, so show it * Not hidden, so show it
*/ */
slices.push(parent) items.push(parent)
} else { } else {
const branch = getBranch(thread, i, item.depth) const branch = getBranch(thread, i, item.depth)
const sortArray = parentMod.muted ? muted : hidden const sortArray = parentMod.muted ? muted : hidden
@@ -191,9 +213,13 @@ export function sort(
for (let ci = startIndex; ci <= branch.end; ci++) { for (let ci = startIndex; ci <= branch.end; ci++) {
const child = thread[ci] const child = thread[ci]
if (AppBskyUnspeccedDefs.isThreadItemPost(child)) { if (
const childPost = views.post({ AppBskyUnspeccedGetPostThreadV2.isThreadItemPost(child.value)
item: child, ) {
const childPost = views.threadPost({
uri: child.uri,
depth: child.depth,
value: child.value,
oneUp: thread[ci - 1], oneUp: thread[ci - 1],
oneDown: thread[ci + 1], oneDown: thread[ci + 1],
moderationOpts, moderationOpts,
@@ -231,7 +257,7 @@ export function sort(
} }
return { return {
slices, items,
hidden, hidden,
muted, muted,
} }
@@ -262,8 +288,6 @@ function getBranch(
for (let ci = branchStartIndex + 1; ci < thread.length; ci++) { for (let ci = branchStartIndex + 1; ci < thread.length; ci++) {
const next = thread[ci] const next = thread[ci]
// ignore unknowns
if (!('depth' in next)) continue
if (next.depth > branchStartDepth) { if (next.depth > branchStartDepth) {
end = ci end = ci
} else { } else {
+17 -9
View File
@@ -1,7 +1,7 @@
import { import {
type AppBskyFeedDefs, type AppBskyFeedDefs,
type AppBskyFeedPost, type AppBskyFeedPost,
type AppBskyUnspeccedDefs, type AppBskyUnspeccedGetPostThreadV2,
type BskyThreadViewPreference, type BskyThreadViewPreference,
type ModerationDecision, type ModerationDecision,
} from '@atproto/api' } from '@atproto/api'
@@ -34,9 +34,11 @@ export enum HiddenReplyKind {
export type Slice = export type Slice =
| { | {
type: 'threadSlice' type: 'threadPost'
key: string key: string
slice: Omit<AppBskyUnspeccedDefs.ThreadItemPost, 'post'> & { uri: string
depth: number
value: Omit<AppBskyUnspeccedGetPostThreadV2.ThreadItemPost, 'post'> & {
post: Omit<AppBskyFeedDefs.PostView, 'record'> & { post: Omit<AppBskyFeedDefs.PostView, 'record'> & {
record: AppBskyFeedPost.Record record: AppBskyFeedPost.Record
} }
@@ -49,19 +51,25 @@ export type Slice =
} }
} }
| { | {
type: 'threadSliceNoUnauthenticated' type: 'threadPostNoUnauthenticated'
key: string key: string
slice: AppBskyUnspeccedDefs.ThreadItemNoUnauthenticated uri: string
depth: number
value: AppBskyUnspeccedGetPostThreadV2.ThreadItemNoUnauthenticated
} }
| { | {
type: 'threadSliceNotFound' type: 'threadPostNotFound'
key: string key: string
slice: AppBskyUnspeccedDefs.ThreadItemNotFound uri: string
depth: number
value: AppBskyUnspeccedGetPostThreadV2.ThreadItemNotFound
} }
| { | {
type: 'threadSliceBlocked' type: 'threadPostBlocked'
key: string key: string
slice: AppBskyUnspeccedDefs.ThreadItemBlocked uri: string
depth: number
value: AppBskyUnspeccedGetPostThreadV2.ThreadItemBlocked
} }
| { | {
type: 'replyComposer' type: 'replyComposer'
+91 -57
View File
@@ -3,7 +3,6 @@ import {
type AppBskyEmbedRecord, type AppBskyEmbedRecord,
type AppBskyFeedDefs, type AppBskyFeedDefs,
type AppBskyFeedPost, type AppBskyFeedPost,
type AppBskyUnspeccedDefs,
type AppBskyUnspeccedGetPostThreadV2, type AppBskyUnspeccedGetPostThreadV2,
moderatePost, moderatePost,
type ModerationOpts, type ModerationOpts,
@@ -12,102 +11,137 @@ import {
import {type Slice} from '#/state/queries/usePostThread/types' import {type Slice} from '#/state/queries/usePostThread/types'
import {embedViewRecordToPostView} from '#/state/queries/util' import {embedViewRecordToPostView} from '#/state/queries/util'
export function noUnauthenticated({ export function threadPostNoUnauthenticated({
item, uri,
}: { depth,
item: AppBskyUnspeccedDefs.ThreadItemNoUnauthenticated value,
}): Extract<Slice, {type: 'threadSliceNoUnauthenticated'}> { }: AppBskyUnspeccedGetPostThreadV2.ThreadItem): Extract<
Slice,
{type: 'threadPostNoUnauthenticated'}
> {
return { return {
type: 'threadSliceNoUnauthenticated', type: 'threadPostNoUnauthenticated',
key: item.uri, key: uri,
slice: item, uri,
depth,
value: value as AppBskyUnspeccedGetPostThreadV2.ThreadItemNoUnauthenticated,
} }
} }
export function notFound({ export function threadPostNotFound({
item, uri,
}: { depth,
item: AppBskyUnspeccedDefs.ThreadItemNotFound value,
}): Extract<Slice, {type: 'threadSliceNotFound'}> { }: AppBskyUnspeccedGetPostThreadV2.ThreadItem): Extract<
Slice,
{type: 'threadPostNotFound'}
> {
return { return {
type: 'threadSliceNotFound', type: 'threadPostNotFound',
key: item.uri, key: uri,
slice: item, uri,
depth,
value: value as AppBskyUnspeccedGetPostThreadV2.ThreadItemNotFound,
} }
} }
export function blocked({ export function threadPostBlocked({
item, uri,
}: { depth,
item: AppBskyUnspeccedDefs.ThreadItemBlocked value,
}): Extract<Slice, {type: 'threadSliceBlocked'}> { }: AppBskyUnspeccedGetPostThreadV2.ThreadItem): Extract<
Slice,
{type: 'threadPostBlocked'}
> {
return { return {
type: 'threadSliceBlocked', type: 'threadPostBlocked',
key: item.uri, key: uri,
slice: item, uri,
depth,
value: value as AppBskyUnspeccedGetPostThreadV2.ThreadItemBlocked,
} }
} }
export function post({ export function threadPost({
item, uri,
depth,
value,
oneUp, oneUp,
oneDown, oneDown,
moderationOpts, moderationOpts,
}: { }: {
item: AppBskyUnspeccedDefs.ThreadItemPost uri: string
depth: number
value: $Typed<AppBskyUnspeccedGetPostThreadV2.ThreadItemPost>
oneUp?: AppBskyUnspeccedGetPostThreadV2.OutputSchema['thread'][number] oneUp?: AppBskyUnspeccedGetPostThreadV2.OutputSchema['thread'][number]
oneDown?: AppBskyUnspeccedGetPostThreadV2.OutputSchema['thread'][number] oneDown?: AppBskyUnspeccedGetPostThreadV2.OutputSchema['thread'][number]
moderationOpts: ModerationOpts moderationOpts: ModerationOpts
}): Extract<Slice, {type: 'threadSlice'}> { }): Extract<Slice, {type: 'threadPost'}> {
return { return {
type: 'threadSlice', type: 'threadPost',
key: item.uri, key: uri,
slice: { uri,
...item, depth,
value: {
...value,
post: { post: {
...item.post, ...value.post,
record: item.post.record as AppBskyFeedPost.Record, record: value.post.record as AppBskyFeedPost.Record,
}, },
}, },
moderation: moderatePost(item.post, moderationOpts), moderation: moderatePost(value.post, moderationOpts),
ui: { ui: {
isAnchor: item.depth === 0, isAnchor: depth === 0,
showParentReplyLine: showParentReplyLine: !!oneUp && oneUp.depth < depth,
!!oneUp && 'depth' in oneUp && oneUp.depth < item.depth, showChildReplyLine: !!oneDown && oneDown.depth > depth,
showChildReplyLine:
!!oneDown && 'depth' in oneDown && oneDown.depth > item.depth,
}, },
} }
} }
export function postViewToThreadPlaceholder( export function postViewToThreadPlaceholder(
post: AppBskyFeedDefs.PostView, post: AppBskyFeedDefs.PostView,
): $Typed<AppBskyUnspeccedDefs.ThreadItemPost> { ): $Typed<
Omit<AppBskyUnspeccedGetPostThreadV2.ThreadItem, 'value'> & {
value: $Typed<AppBskyUnspeccedGetPostThreadV2.ThreadItemPost>
}
> {
return { return {
$type: 'app.bsky.unspecced.defs#threadItemPost', $type: 'app.bsky.unspecced.getPostThreadV2#threadItem',
uri: post.uri, uri: post.uri,
post,
depth: 0, // reset to 0 for highlighted post depth: 0, // reset to 0 for highlighted post
isOPThread: false, // unknown value: {
hasOPLike: false, // unknown $type: 'app.bsky.unspecced.getPostThreadV2#threadItemPost',
hasUnhydratedReplies: false, // unknown post,
// TODO test isOPThread: false, // unknown
hasUnhydratedParents: !!(post.record as AppBskyFeedPost.Record).reply, // unknown hasOPLike: false, // unknown
// @ts-expect-error
hasUnhydratedReplies: false, // unknown
// TODO test
hasUnhydratedParents: !!(post.record as AppBskyFeedPost.Record).reply, // unknown
},
} }
} }
export function embedViewToThreadPlaceholder( export function embedViewToThreadPlaceholder(
record: AppBskyEmbedRecord.ViewRecord, record: AppBskyEmbedRecord.ViewRecord,
): $Typed<AppBskyUnspeccedDefs.ThreadItemPost> { ): $Typed<
Omit<AppBskyUnspeccedGetPostThreadV2.ThreadItem, 'value'> & {
value: $Typed<AppBskyUnspeccedGetPostThreadV2.ThreadItemPost>
}
> {
return { return {
$type: 'app.bsky.unspecced.defs#threadItemPost', $type: 'app.bsky.unspecced.getPostThreadV2#threadItem',
uri: record.uri, uri: record.uri,
post: embedViewRecordToPostView(record),
depth: 0, // reset to 0 for highlighted post depth: 0, // reset to 0 for highlighted post
isOPThread: false, // unknown value: {
hasOPLike: false, // unknown $type: 'app.bsky.unspecced.getPostThreadV2#threadItemPost',
hasUnhydratedReplies: false, // unknown post: embedViewRecordToPostView(record),
// TODO test isOPThread: false, // unknown
hasUnhydratedParents: !!(record.value as AppBskyFeedPost.Record).reply, // unknown hasOPLike: false, // unknown
// @ts-expect-error
hasUnhydratedReplies: false, // unknown
// TODO test
hasUnhydratedParents: !!(record.value as AppBskyFeedPost.Record).reply, // unknown
},
} }
} }
+24 -20
View File
@@ -44,11 +44,10 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {type ImagePickerAsset} from 'expo-image-picker' import {type ImagePickerAsset} from 'expo-image-picker'
import { import {
AppBskyFeedDefs, AppBskyFeedDefs,
AppBskyUnspeccedDefs,
type AppBskyFeedGetPostThread, type AppBskyFeedGetPostThread,
type AppBskyUnspeccedGetPostThreadV2,
type BskyAgent, type BskyAgent,
type RichText, type RichText,
AppBskyFeedGetPostThreadV2,
} from '@atproto/api' } from '@atproto/api'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg, plural, Trans} from '@lingui/macro' import {msg, plural, Trans} from '@lingui/macro'
@@ -57,8 +56,8 @@ import {useQueryClient} from '@tanstack/react-query'
import * as apilib from '#/lib/api/index' import * as apilib from '#/lib/api/index'
import {EmbeddingDisabledError} from '#/lib/api/resolve' import {EmbeddingDisabledError} from '#/lib/api/resolve'
import {until} from '#/lib/async/until'
import {retry} from '#/lib/async/retry' import {retry} from '#/lib/async/retry'
import {until} from '#/lib/async/until'
import { import {
MAX_GRAPHEME_LENGTH, MAX_GRAPHEME_LENGTH,
SUPPORTED_MIME_TYPES, SUPPORTED_MIME_TYPES,
@@ -392,7 +391,7 @@ export const ComposePost = ({
setIsPublishing(true) setIsPublishing(true)
let postUri: string | undefined let postUri: string | undefined
let posts: AppBskyFeedGetPostThreadV2.OutputSchema['thread'] = [] let posts: AppBskyUnspeccedGetPostThreadV2.OutputSchema['thread'] = []
try { try {
postUri = ( postUri = (
await apilib.post(agent, queryClient, { await apilib.post(agent, queryClient, {
@@ -404,22 +403,27 @@ export const ComposePost = ({
).uris[0] ).uris[0]
try { try {
if (postUri) { if (postUri) {
posts = await retry(5, _e => true, async () => { posts = await retry(
const res = await agent.app.bsky.unspecced.getPostThreadV2({ 5,
uri: postUri!, _e => true,
above: 1, async () => {
below: thread.posts.length - 1, const res = await agent.app.bsky.unspecced.getPostThreadV2({
branchingFactor: 1, uri: postUri!,
}) above: 1,
const parent = res.data.thread.at(0) below: thread.posts.length - 1,
if (!AppBskyUnspeccedDefs.isThreadItemPost(parent)) { nestedBranchingFactor: 1,
throw new Error(`Not ready`) })
} const parent = res.data.thread.at(0)
if (res.data.thread.length !== thread.posts.length + 1) { if (!parent) {
throw new Error(`Not ready`) throw new Error(`Not ready`)
} }
return res.data.thread if (res.data.thread.length !== thread.posts.length + 1) {
}, 1e3) throw new Error(`Not ready`)
}
return res.data.thread
},
1e3,
)
} }
await whenAppViewReady(agent, postUri, res => { await whenAppViewReady(agent, postUri, res => {
+35 -31
View File
@@ -1,12 +1,15 @@
import {useCallback, useMemo, useRef, useState} from 'react' import {useCallback, useMemo, useRef, useState} from 'react'
import {useWindowDimensions, View} from 'react-native' import {useWindowDimensions, View} from 'react-native'
import {type AppBskyUnspeccedDefs} from '@atproto/api' import {type AppBskyUnspeccedGetPostThreadV2} from '@atproto/api'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native' import {useFocusEffect} from '@react-navigation/native'
import {HITSLOP_10} from '#/lib/constants' import {HITSLOP_10} from '#/lib/constants'
import {type CommonNavigatorParams, type NativeStackScreenProps} from '#/lib/routes/types' import {
type CommonNavigatorParams,
type NativeStackScreenProps,
} from '#/lib/routes/types'
import {cleanError} from '#/lib/strings/errors' import {cleanError} from '#/lib/strings/errors'
import {makeRecordUri} from '#/lib/strings/url-helpers' import {makeRecordUri} from '#/lib/strings/url-helpers'
import {isNative} from '#/platform/detection' import {isNative} from '#/platform/detection'
@@ -157,31 +160,31 @@ export function Inner({uri}: {uri: string | undefined}) {
const layoutHeaderRef = useRef<View | null>(null) const layoutHeaderRef = useRef<View | null>(null)
const anchorPostRef = useRef<View | null>(null) const anchorPostRef = useRef<View | null>(null)
const optimisticOnPostReply = const optimisticOnPostReply = (
({post}: {post: AppBskyUnspeccedDefs.ThreadItemPost}) => _: any,
(_: any, posts: AppBskyUnspeccedDefs.ThreadItemPost[]) => { posts: AppBskyUnspeccedGetPostThreadV2.ThreadItem[],
if (posts.length) { ) => {
console.log('insert', posts) if (posts.length) {
const parent = posts.at(0) const parent = posts.at(0)
const replies = posts.slice(1) const replies = posts.slice(1)
if (parent && replies.length) { if (parent && replies.length) {
insertReplies(parent, replies) insertReplies(parent, replies)
}
} }
} }
}
const {openComposer} = useOpenComposer() const {openComposer} = useOpenComposer()
const onReplyToAnchor = () => { const onReplyToAnchor = () => {
const anchorPost = data?.slices.find( const anchorPost = data?.items.find(
slice => slice.type === 'threadSlice' && slice.ui.isAnchor, slice => slice.type === 'threadPost' && slice.ui.isAnchor,
) )
if (anchorPost?.type !== 'threadSlice') { if (anchorPost?.type !== 'threadPost') {
return return
} }
const post = anchorPost.slice.post const post = anchorPost.value.post
openComposer({ openComposer({
replyTo: { replyTo: {
uri: anchorPost.slice.uri, uri: anchorPost.uri,
cid: post.cid, cid: post.cid,
text: post.record.text, text: post.record.text,
author: post.author, author: post.author,
@@ -189,42 +192,43 @@ export function Inner({uri}: {uri: string | undefined}) {
moderation: anchorPost.moderation, moderation: anchorPost.moderation,
}, },
// @ts-expect-error TODO // @ts-expect-error TODO
onPost: optimisticOnPostReply({post: anchorPost.slice}), onPost: optimisticOnPostReply,
}) })
} }
const renderItem = ({item, index}: {item: Slice; index: number}) => { const renderItem = ({item, index}: {item: Slice; index: number}) => {
if (item.type === 'threadSlice') { if (item.type === 'threadPost') {
return ( return (
<View ref={item.ui.isAnchor ? anchorPostRef : undefined}> <View ref={item.ui.isAnchor ? anchorPostRef : undefined}>
<PostThreadItem <PostThreadItem
post={item.slice.post} post={item.value.post}
record={item.slice.post.record} record={item.value.post.record}
threadgateRecord={data?.threadgate?.record ?? undefined} threadgateRecord={data?.threadgate?.record ?? undefined}
moderation={item.moderation} moderation={item.moderation}
treeView={treeViewEnabled} treeView={treeViewEnabled}
depth={item.slice.depth} depth={item.depth}
// TODO // TODO
// prevPost={prev} // prevPost={prev}
// nextPost={next} // nextPost={next}
isHighlightedPost={item.ui.isAnchor} isHighlightedPost={item.ui.isAnchor}
hasMore={item.slice.hasUnhydratedReplies} // @ts-expect-error
hasMore={item.value.hasUnhydratedReplies}
showChildReplyLine={item.ui.showChildReplyLine} showChildReplyLine={item.ui.showChildReplyLine}
showParentReplyLine={item.ui.showParentReplyLine} showParentReplyLine={item.ui.showParentReplyLine}
hasPrecedingItem={ hasPrecedingItem={
item.ui.showParentReplyLine || !!item.slice.hasUnhydratedParents // @ts-expect-error
item.ui.showParentReplyLine || !!item.value.hasUnhydratedParents
} // !!hasUnrevealedParents // TODO } // !!hasUnrevealedParents // TODO
overrideBlur={ overrideBlur={
shownHiddenReplyKinds.has(HiddenReplyKind.Muted) && shownHiddenReplyKinds.has(HiddenReplyKind.Muted) && item.depth > 0
item.slice.depth > 0
} }
// @ts-expect-error TODO // @ts-expect-error TODO
onPostReply={optimisticOnPostReply({post: item.slice})} onPostReply={optimisticOnPostReply}
hideTopBorder={index === 0} // && !item.slice.isParentLoading} // TODO hideTopBorder={index === 0} // && !item.isParentLoading} // TODO
/> />
</View> </View>
) )
} else if (item.type === 'threadSliceBlocked') { } else if (item.type === 'threadPostBlocked') {
return ( return (
<View <View
style={[ style={[
@@ -238,7 +242,7 @@ export function Inner({uri}: {uri: string | undefined}) {
</Text> </Text>
</View> </View>
) )
} else if (item.type === 'threadSliceNotFound') { } else if (item.type === 'threadPostNotFound') {
return ( return (
<View <View
style={[ style={[
@@ -332,7 +336,7 @@ export function Inner({uri}: {uri: string | undefined}) {
> >
<List <List
ref={ref} ref={ref}
data={data?.slices ?? []} data={data?.items ?? []}
renderItem={renderItem} renderItem={renderItem}
keyExtractor={keyExtractor} keyExtractor={keyExtractor}
onContentSizeChange={isNative ? undefined : onContentSizeChangeWeb} onContentSizeChange={isNative ? undefined : onContentSizeChangeWeb}