1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
|
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 (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 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,
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,
repliesIndexCounter: 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,
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: 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),
}
}
|