186 lines
4.9 KiB
TypeScript
186 lines
4.9 KiB
TypeScript
import {
|
|
type AppBskyFeedDefs,
|
|
AppBskyFeedPost,
|
|
AppBskyFeedThreadgate,
|
|
AppBskyUnspeccedDefs,
|
|
type AppBskyUnspeccedGetPostThreadV2,
|
|
AtUri,
|
|
} from '@atproto/api'
|
|
|
|
import {
|
|
type ApiThreadItem,
|
|
type ThreadItem,
|
|
type TraversalMetadata,
|
|
} from '#/state/queries/usePostThread/types'
|
|
import {isDevMode} from '#/storage/hooks/dev-mode'
|
|
import * as bsky from '#/types/bsky'
|
|
|
|
export function getThreadgateRecord(
|
|
view: AppBskyUnspeccedGetPostThreadV2.OutputSchema['threadgate'],
|
|
) {
|
|
return bsky.dangerousIsType<AppBskyFeedThreadgate.Record>(
|
|
view?.record,
|
|
AppBskyFeedThreadgate.isRecord,
|
|
)
|
|
? view?.record
|
|
: undefined
|
|
}
|
|
|
|
export function getRootPostAtUri(post: AppBskyFeedDefs.PostView) {
|
|
if (
|
|
bsky.dangerousIsType<AppBskyFeedPost.Record>(
|
|
post.record,
|
|
AppBskyFeedPost.isRecord,
|
|
)
|
|
) {
|
|
/**
|
|
* If the record has no `reply` field, it is a root post.
|
|
*/
|
|
if (!post.record.reply) {
|
|
return new AtUri(post.uri)
|
|
}
|
|
if (post.record.reply?.root?.uri) {
|
|
return new AtUri(post.record.reply.root.uri)
|
|
}
|
|
}
|
|
}
|
|
|
|
export function getPostRecord(post: AppBskyFeedDefs.PostView) {
|
|
return post.record as AppBskyFeedPost.Record
|
|
}
|
|
|
|
export function getTraversalMetadata({
|
|
item,
|
|
prevItem,
|
|
nextItem,
|
|
parentMetadata,
|
|
}: {
|
|
item: ApiThreadItem
|
|
prevItem?: ApiThreadItem
|
|
nextItem?: ApiThreadItem
|
|
parentMetadata?: TraversalMetadata
|
|
}): TraversalMetadata {
|
|
if (!AppBskyUnspeccedDefs.isThreadItemPost(item.value)) {
|
|
throw new Error(`Expected thread item to be a post`)
|
|
}
|
|
const rootUri = getRootPostAtUri(item.value.post)
|
|
const rootAuthorDid = rootUri?.host
|
|
const isRoot = item.uri === item.value.post.uri
|
|
const isPartOfOPThreadFromRoot = Boolean(
|
|
item.value.post.author.did === rootAuthorDid &&
|
|
(parentMetadata?.isRoot || parentMetadata?.isPartOfOPThreadFromRoot),
|
|
)
|
|
const repliesCount = item.value.post.replyCount || 0
|
|
const repliesUnhydrated = item.value.moreReplies || 0
|
|
const metadata = {
|
|
depth: item.depth,
|
|
/*
|
|
* Unknown until after traversal
|
|
*/
|
|
isLastChild: false,
|
|
/*
|
|
* Unknown until after traversal
|
|
*/
|
|
isLastSibling: false,
|
|
/*
|
|
* If it's a top level reply, bc we render each top-level branch as a
|
|
* separate tree, it's implicitly part of the last branch. For subsequent
|
|
* replies, we'll override this after traversal.
|
|
*/
|
|
isPartOfLastBranchFromDepth: item.depth === 1 ? 1 : undefined,
|
|
isPartOfOPThreadFromRoot,
|
|
isRoot,
|
|
nextItemDepth: nextItem?.depth,
|
|
parentMetadata,
|
|
prevItemDepth: prevItem?.depth,
|
|
/*
|
|
* Unknown until after traversal
|
|
*/
|
|
precedesChildReadMore: false,
|
|
/*
|
|
* Unknown until after traversal
|
|
*/
|
|
followsReadMoreUp: false,
|
|
postData: {
|
|
uri: item.uri,
|
|
authorHandle: item.value.post.author.handle,
|
|
},
|
|
repliesCount,
|
|
repliesUnhydrated,
|
|
repliesSeenCounter: 0,
|
|
replyIndex: 0,
|
|
skippedIndentIndices: new Set<number>(),
|
|
}
|
|
|
|
if (isDevMode()) {
|
|
// @ts-ignore dev only for debugging
|
|
metadata.postData.text = getPostRecord(item.value.post).text
|
|
}
|
|
|
|
return metadata
|
|
}
|
|
|
|
export function storeTraversalMetadata(
|
|
metadatas: Map<string, TraversalMetadata>,
|
|
metadata: TraversalMetadata,
|
|
) {
|
|
metadatas.set(metadata.postData.uri, metadata)
|
|
|
|
if (isDevMode()) {
|
|
// @ts-ignore dev only for debugging
|
|
metadatas.set(metadata.postData.text, metadata)
|
|
// @ts-ignore
|
|
window.__thread = metadatas
|
|
}
|
|
}
|
|
|
|
export function getThreadPostUI({
|
|
depth,
|
|
repliesCount,
|
|
prevItemDepth,
|
|
isLastChild,
|
|
isPartOfOPThreadFromRoot,
|
|
skippedIndentIndices,
|
|
repliesSeenCounter,
|
|
repliesUnhydrated,
|
|
precedesChildReadMore,
|
|
followsReadMoreUp,
|
|
}: TraversalMetadata): Extract<ThreadItem, {type: 'threadPost'}>['ui'] {
|
|
const isReplyAndHasReplies =
|
|
depth > 0 &&
|
|
repliesCount > 0 &&
|
|
(repliesCount - repliesUnhydrated === repliesSeenCounter ||
|
|
repliesSeenCounter > 0)
|
|
return {
|
|
isAnchor: depth === 0,
|
|
showParentReplyLine:
|
|
followsReadMoreUp ||
|
|
(!!prevItemDepth && prevItemDepth !== 0 && prevItemDepth < depth),
|
|
showChildReplyLine: depth < 0 || isReplyAndHasReplies,
|
|
indent: isPartOfOPThreadFromRoot ? (depth < 1 ? 0 : 1) : depth,
|
|
/*
|
|
* If there are no slices below this one, or the next slice has a depth <=
|
|
* than the depth of this post, it's the last child of the reply tree. It
|
|
* is not necessarily the last leaf in the parent branch, since it could
|
|
* have another sibling.
|
|
*/
|
|
isLastChild,
|
|
skippedIndentIndices,
|
|
precedesChildReadMore: precedesChildReadMore ?? false,
|
|
}
|
|
}
|
|
|
|
export function getThreadPostNoUnauthenticatedUI({
|
|
depth,
|
|
prevItemDepth,
|
|
}: {
|
|
depth: number
|
|
prevItemDepth?: number
|
|
nextItemDepth?: number
|
|
}): Extract<ThreadItem, {type: 'threadPostNoUnauthenticated'}>['ui'] {
|
|
return {
|
|
showChildReplyLine: depth < 0,
|
|
showParentReplyLine: Boolean(prevItemDepth && prevItemDepth < depth),
|
|
}
|
|
}
|