Re-org
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
import {useQuery, useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {getThreadPlaceholder} from '#/state/queries/usePostThread/queryCache'
|
||||
import {flatten,sort} from '#/state/queries/usePostThread/traversal'
|
||||
import {
|
||||
createPostThreadQueryKey,
|
||||
HiddenReplyKind,
|
||||
type UsePostThreadProps,
|
||||
} from '#/state/queries/usePostThread/types'
|
||||
import {
|
||||
getThreadgateRecord,
|
||||
mapSortOptionsToSortID,
|
||||
} from '#/state/queries/usePostThread/utils'
|
||||
import {useAgent, useSession} from '#/state/session'
|
||||
import {useMergeThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
|
||||
|
||||
export * from '#/state/queries/usePostThread/types'
|
||||
|
||||
export function usePostThread({
|
||||
uri,
|
||||
enabled: isEnabled,
|
||||
params,
|
||||
state,
|
||||
}: UsePostThreadProps) {
|
||||
const qc = useQueryClient()
|
||||
const agent = useAgent()
|
||||
const {hasSession} = useSession()
|
||||
const moderationOpts = useModerationOpts()
|
||||
const mergeThreadgateHiddenReplies = useMergeThreadgateHiddenReplies()
|
||||
|
||||
const enabled = isEnabled !== false && !!uri && !!moderationOpts
|
||||
|
||||
const query = useQuery({
|
||||
enabled,
|
||||
queryKey: createPostThreadQueryKey({
|
||||
uri,
|
||||
params,
|
||||
}),
|
||||
async queryFn() {
|
||||
const {data} = await agent.app.bsky.unspecced.getPostThreadV2({
|
||||
uri: uri!,
|
||||
branchingFactor: params.view === 'linear' ? 1 : 10,
|
||||
below: 10,
|
||||
sorting: mapSortOptionsToSortID(params.sort),
|
||||
})
|
||||
return data
|
||||
},
|
||||
placeholderData() {
|
||||
if (!uri) return
|
||||
const placeholder = getThreadPlaceholder(qc, uri)
|
||||
if (placeholder) {
|
||||
return {thread: [placeholder]}
|
||||
}
|
||||
return
|
||||
},
|
||||
select(data) {
|
||||
const threadgate = getThreadgateRecord(data.threadgate)
|
||||
return {
|
||||
...data,
|
||||
threadgate: {
|
||||
...data.threadgate,
|
||||
record: threadgate,
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// TODO map over pages, just like feeds
|
||||
|
||||
const items = flatten(
|
||||
sort(query.data?.thread || [], {
|
||||
threadgateHiddenReplies: mergeThreadgateHiddenReplies(
|
||||
query.data?.threadgate?.record,
|
||||
),
|
||||
moderationOpts: moderationOpts!,
|
||||
}),
|
||||
{
|
||||
hasSession,
|
||||
showMuted: state.shownHiddenReplyKinds.has(HiddenReplyKind.Muted),
|
||||
showHidden: state.shownHiddenReplyKinds.has(HiddenReplyKind.Hidden),
|
||||
},
|
||||
)
|
||||
|
||||
return {
|
||||
...query,
|
||||
data: {
|
||||
slices: items,
|
||||
threadgate: query.data?.threadgate,
|
||||
},
|
||||
insertReplies: () => {},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import {
|
||||
type $Typed,
|
||||
AppBskyUnspeccedDefs,
|
||||
type AppBskyUnspeccedGetPostThreadV2,
|
||||
AtUri,
|
||||
} from '@atproto/api'
|
||||
import {type QueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {findAllPostsInQueryData as findAllPostsInExploreFeedPreviewsQueryData} 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 findAllPostsInQuoteQueryData} from '#/state/queries/post-quotes'
|
||||
import {findAllPostsInQueryData as findAllPostsInSearchQueryData} from '#/state/queries/search-posts'
|
||||
import {postThreadQueryKeyRoot} from '#/state/queries/usePostThread/types'
|
||||
import {
|
||||
embedViewToThreadPlaceholder,
|
||||
postViewToThreadPlaceholder,
|
||||
} from '#/state/queries/usePostThread/views'
|
||||
import {didOrHandleUriMatches, getEmbeddedPost} from '#/state/queries/util'
|
||||
|
||||
export function getThreadPlaceholder(
|
||||
queryClient: QueryClient,
|
||||
uri: string,
|
||||
): $Typed<AppBskyUnspeccedDefs.ThreadItemPost> | void {
|
||||
let partial
|
||||
for (let item of getThreadPlaceholderCandidates(queryClient, uri)) {
|
||||
/*
|
||||
* 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
|
||||
* detect that. In the future, we should fix this on the backend, which
|
||||
* will let us always stop on the first result.
|
||||
*
|
||||
* TODO can we send in feeds and quotes?
|
||||
*/
|
||||
const hasAllInfo = item.post.likeCount != null
|
||||
if (hasAllInfo) {
|
||||
return item
|
||||
} else {
|
||||
// Keep searching, we might still find a full post in the cache.
|
||||
partial = item
|
||||
}
|
||||
}
|
||||
return partial
|
||||
}
|
||||
|
||||
export function* getThreadPlaceholderCandidates(
|
||||
queryClient: QueryClient,
|
||||
uri: string,
|
||||
): Generator<$Typed<AppBskyUnspeccedDefs.ThreadItemPost>, void> {
|
||||
const atUri = new AtUri(uri)
|
||||
|
||||
/*
|
||||
* Check this thread in the cache first.
|
||||
* TODO extract just this for shadowing
|
||||
*/
|
||||
const queryDatas =
|
||||
queryClient.getQueriesData<AppBskyUnspeccedGetPostThreadV2.OutputSchema>({
|
||||
queryKey: [postThreadQueryKeyRoot],
|
||||
})
|
||||
for (const [_queryKey, queryData] of queryDatas) {
|
||||
if (!queryData) continue
|
||||
|
||||
const {thread} = queryData
|
||||
|
||||
for (const item of thread) {
|
||||
if (AppBskyUnspeccedDefs.isThreadItemPost(item)) {
|
||||
if (didOrHandleUriMatches(atUri, item.post)) {
|
||||
yield {
|
||||
...item,
|
||||
depth: 0,
|
||||
}
|
||||
}
|
||||
|
||||
const qp = getEmbeddedPost(item.post.embed)
|
||||
if (qp && didOrHandleUriMatches(atUri, qp)) {
|
||||
yield embedViewToThreadPlaceholder(qp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Check notifications first. If you have a post in notifications, it's
|
||||
* often due to a like or a repost, and we want to prioritize a post object
|
||||
* with >0 likes/reposts over a stale version with no metrics in order to
|
||||
* avoid a notification->post scroll jump.
|
||||
*/
|
||||
for (let post of findAllPostsInNotifsQueryData(queryClient, uri)) {
|
||||
yield postViewToThreadPlaceholder(post)
|
||||
}
|
||||
for (let post of findAllPostsInFeedQueryData(queryClient, uri)) {
|
||||
yield postViewToThreadPlaceholder(post)
|
||||
}
|
||||
for (let post of findAllPostsInQuoteQueryData(queryClient, uri)) {
|
||||
yield postViewToThreadPlaceholder(post)
|
||||
}
|
||||
for (let post of findAllPostsInSearchQueryData(queryClient, uri)) {
|
||||
yield postViewToThreadPlaceholder(post)
|
||||
}
|
||||
for (let post of findAllPostsInExploreFeedPreviewsQueryData(
|
||||
queryClient,
|
||||
uri,
|
||||
)) {
|
||||
yield postViewToThreadPlaceholder(post)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
import {
|
||||
AppBskyUnspeccedDefs,
|
||||
type AppBskyUnspeccedGetPostThreadV2,
|
||||
type ModerationDecision,
|
||||
type ModerationOpts,
|
||||
} from '@atproto/api'
|
||||
|
||||
import {HiddenReplyKind,type Slice} from '#/state/queries/usePostThread/types'
|
||||
import * as views from '#/state/queries/usePostThread/views'
|
||||
|
||||
export function flatten(
|
||||
sorted: ReturnType<typeof sort>,
|
||||
{
|
||||
hasSession,
|
||||
showMuted,
|
||||
showHidden,
|
||||
}: {
|
||||
hasSession: boolean
|
||||
showMuted: boolean
|
||||
showHidden: boolean
|
||||
},
|
||||
) {
|
||||
const flattened: Slice[] = sorted.slices
|
||||
|
||||
if (sorted.hidden.length) {
|
||||
if (showHidden) {
|
||||
flattened.push(...sorted.hidden)
|
||||
|
||||
if (sorted.muted.length) {
|
||||
if (showMuted) {
|
||||
flattened.push(...sorted.muted)
|
||||
} else {
|
||||
flattened.push({
|
||||
type: 'showHiddenReplies',
|
||||
key: 'showMutedReplies',
|
||||
kind: HiddenReplyKind.Muted,
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
flattened.push({
|
||||
type: 'showHiddenReplies',
|
||||
key: 'showHiddenReplies',
|
||||
kind: HiddenReplyKind.Hidden,
|
||||
})
|
||||
}
|
||||
} else if (sorted.muted.length) {
|
||||
if (showMuted) {
|
||||
flattened.push(...sorted.muted)
|
||||
} else {
|
||||
flattened.push({
|
||||
type: 'showHiddenReplies',
|
||||
key: 'showMutedReplies',
|
||||
kind: HiddenReplyKind.Muted,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (hasSession) {
|
||||
for (let i = 0; i < flattened.length; i++) {
|
||||
const slice = flattened[i]
|
||||
if ('slice' in slice && slice.slice.depth === 0) {
|
||||
flattened.splice(i + 1, 0, {
|
||||
type: 'replyComposer',
|
||||
key: 'replyComposer',
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return flattened
|
||||
}
|
||||
|
||||
export function sort(
|
||||
thread: AppBskyUnspeccedGetPostThreadV2.OutputSchema['thread'],
|
||||
{
|
||||
threadgateHiddenReplies,
|
||||
moderationOpts,
|
||||
}: {
|
||||
threadgateHiddenReplies: Set<string>
|
||||
moderationOpts: ModerationOpts
|
||||
},
|
||||
) {
|
||||
const slices: Slice[] = []
|
||||
const hidden: Slice[] = []
|
||||
const muted: Slice[] = []
|
||||
|
||||
traversal: for (let i = 0; i < thread.length; i++) {
|
||||
const item = thread[i]
|
||||
|
||||
// ignore unknowns
|
||||
if (!('depth' in item)) continue
|
||||
|
||||
if (item.depth < 0) {
|
||||
/*
|
||||
* Parents are ignored until we find the highlighted post, then we walk
|
||||
* _up_ from there.
|
||||
*/
|
||||
} else if (item.depth === 0) {
|
||||
if (AppBskyUnspeccedDefs.isThreadItemNoUnauthenticated(item)) {
|
||||
slices.push(views.noUnauthenticated({item}))
|
||||
} else if (AppBskyUnspeccedDefs.isThreadItemNotFound(item)) {
|
||||
slices.push(views.notFound({item}))
|
||||
} else if (AppBskyUnspeccedDefs.isThreadItemBlocked(item)) {
|
||||
slices.push(views.blocked({item}))
|
||||
} else if (AppBskyUnspeccedDefs.isThreadItemPost(item)) {
|
||||
slices.push(
|
||||
views.post({
|
||||
item,
|
||||
oneUp: thread[i - 1],
|
||||
oneDown: thread[i + 1],
|
||||
moderationOpts,
|
||||
}),
|
||||
)
|
||||
|
||||
parentTraversal: for (let pi = i - 1; pi >= 0; pi--) {
|
||||
const parentOneDown = thread[pi + 1]
|
||||
const parent = thread[pi]
|
||||
const parentOneUp = thread[pi - 1]
|
||||
|
||||
if (AppBskyUnspeccedDefs.isThreadItemNoUnauthenticated(parent)) {
|
||||
slices.unshift(views.noUnauthenticated({item: parent}))
|
||||
break parentTraversal
|
||||
} else if (AppBskyUnspeccedDefs.isThreadItemNotFound(parent)) {
|
||||
slices.unshift(views.notFound({item: parent}))
|
||||
break parentTraversal
|
||||
} else if (AppBskyUnspeccedDefs.isThreadItemBlocked(parent)) {
|
||||
slices.unshift(views.blocked({item: parent}))
|
||||
break parentTraversal
|
||||
} else if (AppBskyUnspeccedDefs.isThreadItemPost(parent)) {
|
||||
slices.unshift(
|
||||
views.post({
|
||||
item: parent,
|
||||
oneUp: parentOneUp,
|
||||
oneDown: parentOneDown,
|
||||
moderationOpts,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (item.depth > 0) {
|
||||
/*
|
||||
* The API does not send down any unavailable replies, so this will
|
||||
* always be false (for now). If we ever wanted to tombstone them here,
|
||||
* we could.
|
||||
*/
|
||||
const shouldBreak =
|
||||
AppBskyUnspeccedDefs.isThreadItemNoUnauthenticated(item) ||
|
||||
AppBskyUnspeccedDefs.isThreadItemNotFound(item) ||
|
||||
AppBskyUnspeccedDefs.isThreadItemBlocked(item)
|
||||
|
||||
if (shouldBreak) {
|
||||
const branch = getBranch(thread, i, item.depth)
|
||||
// could insert tombstone
|
||||
i = branch.end
|
||||
continue traversal
|
||||
} else if (AppBskyUnspeccedDefs.isThreadItemPost(item)) {
|
||||
const lastSlice = slices[slices.length - 1]
|
||||
const isFirstReply =
|
||||
lastSlice.type === 'replyComposer' ||
|
||||
(lastSlice.type === 'threadSlice' && lastSlice.slice.depth === 0)
|
||||
const parent = views.post({
|
||||
item,
|
||||
oneUp: isFirstReply ? undefined : thread[i - 1],
|
||||
oneDown: thread[i + 1],
|
||||
moderationOpts,
|
||||
})
|
||||
const parentMod = getModerationState(parent.moderation)
|
||||
const parentIsHidden = threadgateHiddenReplies.has(item.uri)
|
||||
const parentIsTopLevelReply = item.depth === 1
|
||||
const parentIsModerated =
|
||||
parentIsHidden || parentMod.blurred || parentMod.muted
|
||||
|
||||
if (!parentIsModerated) {
|
||||
/*
|
||||
* Not hidden, so show it
|
||||
*/
|
||||
slices.push(parent)
|
||||
} else {
|
||||
const branch = getBranch(thread, i, item.depth)
|
||||
const sortArray = parentMod.muted ? muted : hidden
|
||||
|
||||
if (parentIsTopLevelReply) {
|
||||
// push branch anchor into sorted array
|
||||
sortArray.push(parent)
|
||||
// skip branch anchor in branch traversal
|
||||
const startIndex = branch.start + 1
|
||||
|
||||
for (let ci = startIndex; ci <= branch.end; ci++) {
|
||||
const child = thread[ci]
|
||||
|
||||
if (AppBskyUnspeccedDefs.isThreadItemPost(child)) {
|
||||
const childPost = views.post({
|
||||
item: child,
|
||||
oneUp: thread[ci - 1],
|
||||
oneDown: thread[ci + 1],
|
||||
moderationOpts,
|
||||
})
|
||||
const childMod = getModerationState(childPost.moderation)
|
||||
const childIsHidden = threadgateHiddenReplies.has(child.uri)
|
||||
|
||||
/*
|
||||
* If a child is hidden in any way, drop it an its sub-branch
|
||||
* entirely. To reveal these, the user must navigate to the
|
||||
* parent post directly.
|
||||
*/
|
||||
if (childMod.blurred || childMod.muted || childIsHidden) {
|
||||
ci = getBranch(thread, ci, child.depth).end
|
||||
} else {
|
||||
sortArray.push(childPost)
|
||||
}
|
||||
} else {
|
||||
/*
|
||||
* Drop the rest of the branch if we hit anything unexpected
|
||||
*/
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Skip to next branch
|
||||
*/
|
||||
i = branch.end
|
||||
continue traversal
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
slices,
|
||||
hidden,
|
||||
muted,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the start and end index of a "branch" of the thread. A "branch" is a
|
||||
* parent and it's children (not siblings). Returned indices are inclusive of
|
||||
* the parent and its last child.
|
||||
*
|
||||
* items[] (index, depth)
|
||||
* ├── branch ───── (0, 1)
|
||||
* ├─┬ branch ───── (1, 1) (start)
|
||||
* │ ├──┬ leaf ──── (2, 2)
|
||||
* │ │ └── leaf ── (3, 3)
|
||||
* │ └── leaf ───── (4, 2) (end)
|
||||
* ├── branch ───── (5, 1)
|
||||
* ├── branch ───── (6, 1)
|
||||
*
|
||||
* const { start: 1, end: 3 } = getBranch(items, 1, 1)
|
||||
*/
|
||||
function getBranch(
|
||||
thread: AppBskyUnspeccedGetPostThreadV2.OutputSchema['thread'],
|
||||
branchStartIndex: number,
|
||||
branchStartDepth: number,
|
||||
) {
|
||||
let end = branchStartIndex
|
||||
|
||||
for (let ci = branchStartIndex + 1; ci < thread.length; ci++) {
|
||||
const next = thread[ci]
|
||||
// ignore unknowns
|
||||
if (!('depth' in next)) continue
|
||||
if (next.depth > branchStartDepth) {
|
||||
end = ci
|
||||
} else {
|
||||
end = ci - 1
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
start: branchStartIndex,
|
||||
end,
|
||||
}
|
||||
}
|
||||
|
||||
export function getModerationState(moderation: ModerationDecision) {
|
||||
const modui = moderation.ui('contentList')
|
||||
const blurred = modui.blur || modui.filter
|
||||
const muted = (modui.blurs[0] || modui.filters[0])?.type === 'muted'
|
||||
return {
|
||||
blurred,
|
||||
muted,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import {
|
||||
type AppBskyFeedDefs,
|
||||
type AppBskyFeedPost,
|
||||
type AppBskyUnspeccedDefs,
|
||||
type BskyThreadViewPreference,
|
||||
type ModerationDecision,
|
||||
} from '@atproto/api'
|
||||
|
||||
export const postThreadQueryKeyRoot = 'getPostThreadV2' as const
|
||||
|
||||
export const createPostThreadQueryKey = (
|
||||
props: Pick<UsePostThreadProps, 'uri' | 'params'>,
|
||||
) => [postThreadQueryKeyRoot, props] as const
|
||||
|
||||
export type PostThreadParams = {
|
||||
view: 'tree' | 'linear'
|
||||
sort: 'hotness' | 'oldest' | 'newest' | 'most-likes' | 'random' | string
|
||||
prioritizeFollows: BskyThreadViewPreference['prioritizeFollowedUsers']
|
||||
}
|
||||
|
||||
export type UsePostThreadProps = {
|
||||
uri?: string
|
||||
enabled?: boolean
|
||||
params: PostThreadParams
|
||||
state: {
|
||||
shownHiddenReplyKinds: Set<HiddenReplyKind>
|
||||
}
|
||||
}
|
||||
|
||||
export enum HiddenReplyKind {
|
||||
Hidden = 'hidden',
|
||||
Muted = 'muted',
|
||||
}
|
||||
|
||||
export type Slice =
|
||||
| {
|
||||
type: 'threadSlice'
|
||||
key: string
|
||||
slice: Omit<AppBskyUnspeccedDefs.ThreadItemPost, 'post'> & {
|
||||
post: Omit<AppBskyFeedDefs.PostView, 'record'> & {
|
||||
record: AppBskyFeedPost.Record
|
||||
}
|
||||
}
|
||||
moderation: ModerationDecision
|
||||
ui: {
|
||||
isAnchor: boolean
|
||||
showParentReplyLine: boolean
|
||||
showChildReplyLine: boolean
|
||||
}
|
||||
}
|
||||
| {
|
||||
type: 'threadSliceNoUnauthenticated'
|
||||
key: string
|
||||
slice: AppBskyUnspeccedDefs.ThreadItemNoUnauthenticated
|
||||
}
|
||||
| {
|
||||
type: 'threadSliceNotFound'
|
||||
key: string
|
||||
slice: AppBskyUnspeccedDefs.ThreadItemNotFound
|
||||
}
|
||||
| {
|
||||
type: 'threadSliceBlocked'
|
||||
key: string
|
||||
slice: AppBskyUnspeccedDefs.ThreadItemBlocked
|
||||
}
|
||||
| {
|
||||
type: 'replyComposer'
|
||||
key: string
|
||||
}
|
||||
| {
|
||||
type: 'showHiddenReplies'
|
||||
key: string
|
||||
kind: HiddenReplyKind
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import {
|
||||
APP_BSKY_UNSPECCED,
|
||||
AppBskyFeedThreadgate,
|
||||
type AppBskyUnspeccedGetPostThreadV2,
|
||||
} from '@atproto/api'
|
||||
|
||||
import {type PostThreadParams} from '#/state/queries/usePostThread/types'
|
||||
import * as bsky from '#/types/bsky'
|
||||
|
||||
export function mapSortOptionsToSortID(sort: PostThreadParams['sort']) {
|
||||
switch (sort) {
|
||||
case 'hotness':
|
||||
return APP_BSKY_UNSPECCED.GetPostThreadV2Hotness
|
||||
case 'oldest':
|
||||
return APP_BSKY_UNSPECCED.GetPostThreadV2Oldest
|
||||
case 'newest':
|
||||
return APP_BSKY_UNSPECCED.GetPostThreadV2Newest
|
||||
case 'most-likes':
|
||||
return APP_BSKY_UNSPECCED.GetPostThreadV2MostLikes
|
||||
default:
|
||||
return APP_BSKY_UNSPECCED.GetPostThreadV2Hotness
|
||||
}
|
||||
}
|
||||
|
||||
export function getThreadgateRecord(
|
||||
view: AppBskyUnspeccedGetPostThreadV2.OutputSchema['threadgate'],
|
||||
) {
|
||||
return bsky.dangerousIsType<AppBskyFeedThreadgate.Record>(
|
||||
view?.record,
|
||||
AppBskyFeedThreadgate.isRecord,
|
||||
)
|
||||
? view?.record
|
||||
: undefined
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import {
|
||||
type $Typed,
|
||||
type AppBskyEmbedRecord,
|
||||
type AppBskyFeedDefs,
|
||||
type AppBskyFeedPost,
|
||||
type AppBskyUnspeccedDefs,
|
||||
type AppBskyUnspeccedGetPostThreadV2,
|
||||
moderatePost,
|
||||
type ModerationOpts,
|
||||
} from '@atproto/api'
|
||||
|
||||
import {type Slice} from '#/state/queries/usePostThread/types'
|
||||
import {embedViewRecordToPostView} from '#/state/queries/util'
|
||||
|
||||
export function noUnauthenticated({
|
||||
item,
|
||||
}: {
|
||||
item: AppBskyUnspeccedDefs.ThreadItemNoUnauthenticated
|
||||
}): Extract<Slice, {type: 'threadSliceNoUnauthenticated'}> {
|
||||
return {
|
||||
type: 'threadSliceNoUnauthenticated',
|
||||
key: item.uri,
|
||||
slice: item,
|
||||
}
|
||||
}
|
||||
|
||||
export function notFound({
|
||||
item,
|
||||
}: {
|
||||
item: AppBskyUnspeccedDefs.ThreadItemNotFound
|
||||
}): Extract<Slice, {type: 'threadSliceNotFound'}> {
|
||||
return {
|
||||
type: 'threadSliceNotFound',
|
||||
key: item.uri,
|
||||
slice: item,
|
||||
}
|
||||
}
|
||||
|
||||
export function blocked({
|
||||
item,
|
||||
}: {
|
||||
item: AppBskyUnspeccedDefs.ThreadItemBlocked
|
||||
}): Extract<Slice, {type: 'threadSliceBlocked'}> {
|
||||
return {
|
||||
type: 'threadSliceBlocked',
|
||||
key: item.uri,
|
||||
slice: item,
|
||||
}
|
||||
}
|
||||
|
||||
export function post({
|
||||
item,
|
||||
oneUp,
|
||||
oneDown,
|
||||
moderationOpts,
|
||||
}: {
|
||||
item: AppBskyUnspeccedDefs.ThreadItemPost
|
||||
oneUp?: AppBskyUnspeccedGetPostThreadV2.OutputSchema['thread'][number]
|
||||
oneDown?: AppBskyUnspeccedGetPostThreadV2.OutputSchema['thread'][number]
|
||||
moderationOpts: ModerationOpts
|
||||
}): Extract<Slice, {type: 'threadSlice'}> {
|
||||
return {
|
||||
type: 'threadSlice',
|
||||
key: item.uri,
|
||||
slice: {
|
||||
...item,
|
||||
post: {
|
||||
...item.post,
|
||||
record: item.post.record as AppBskyFeedPost.Record,
|
||||
},
|
||||
},
|
||||
moderation: moderatePost(item.post, moderationOpts),
|
||||
ui: {
|
||||
isAnchor: item.depth === 0,
|
||||
showParentReplyLine:
|
||||
!!oneUp && 'depth' in oneUp && oneUp.depth < item.depth,
|
||||
showChildReplyLine:
|
||||
!!oneDown && 'depth' in oneDown && oneDown.depth > item.depth,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function postViewToThreadPlaceholder(
|
||||
post: AppBskyFeedDefs.PostView,
|
||||
): $Typed<AppBskyUnspeccedDefs.ThreadItemPost> {
|
||||
return {
|
||||
$type: 'app.bsky.unspecced.defs#threadItemPost',
|
||||
uri: post.uri,
|
||||
post,
|
||||
depth: 0, // reset to 0 for highlighted post
|
||||
isOPThread: false, // unknown
|
||||
hasOPLike: false, // unknown
|
||||
hasUnhydratedReplies: false, // unknown
|
||||
// TODO test
|
||||
hasUnhydratedParents: !!(post.record as AppBskyFeedPost.Record).reply, // unknown
|
||||
}
|
||||
}
|
||||
|
||||
export function embedViewToThreadPlaceholder(
|
||||
record: AppBskyEmbedRecord.ViewRecord,
|
||||
): $Typed<AppBskyUnspeccedDefs.ThreadItemPost> {
|
||||
return {
|
||||
$type: 'app.bsky.unspecced.defs#threadItemPost',
|
||||
uri: record.uri,
|
||||
post: embedViewRecordToPostView(record),
|
||||
depth: 0, // reset to 0 for highlighted post
|
||||
isOPThread: false, // unknown
|
||||
hasOPLike: false, // unknown
|
||||
hasUnhydratedReplies: false, // unknown
|
||||
// TODO test
|
||||
hasUnhydratedParents: !!(record.value as AppBskyFeedPost.Record).reply, // unknown
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user