forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {
2 type AppBskyFeedDefs,
3 AppBskyFeedPost,
4 AppBskyFeedThreadgate,
5 AppBskyUnspeccedDefs,
6 type AppBskyUnspeccedGetPostThreadV2,
7 AtUri,
8} from '@atproto/api'
9
10import {
11 type ApiThreadItem,
12 type ThreadItem,
13 type TraversalMetadata,
14} from '#/state/queries/usePostThread/types'
15import {isDevMode} from '#/storage/hooks/dev-mode'
16import * as bsky from '#/types/bsky'
17
18export function getThreadgateRecord(
19 view: AppBskyUnspeccedGetPostThreadV2.OutputSchema['threadgate'],
20) {
21 return bsky.dangerousIsType<AppBskyFeedThreadgate.Record>(
22 view?.record,
23 AppBskyFeedThreadgate.isRecord,
24 )
25 ? view?.record
26 : undefined
27}
28
29export function getRootPostAtUri(post: AppBskyFeedDefs.PostView) {
30 if (
31 bsky.dangerousIsType<AppBskyFeedPost.Record>(
32 post.record,
33 AppBskyFeedPost.isRecord,
34 )
35 ) {
36 /**
37 * If the record has no `reply` field, it is a root post.
38 */
39 if (!post.record.reply) {
40 return new AtUri(post.uri)
41 }
42 if (post.record.reply?.root?.uri) {
43 return new AtUri(post.record.reply.root.uri)
44 }
45 }
46}
47
48export function getPostRecord(post: AppBskyFeedDefs.PostView) {
49 return post.record as AppBskyFeedPost.Record
50}
51
52export function getTraversalMetadata({
53 item,
54 prevItem,
55 nextItem,
56 parentMetadata,
57}: {
58 item: ApiThreadItem
59 prevItem?: ApiThreadItem
60 nextItem?: ApiThreadItem
61 parentMetadata?: TraversalMetadata
62}): TraversalMetadata {
63 if (!AppBskyUnspeccedDefs.isThreadItemPost(item.value)) {
64 throw new Error(`Expected thread item to be a post`)
65 }
66 const repliesCount = item.value.post.replyCount || 0
67 const repliesUnhydrated = item.value.moreReplies || 0
68 const metadata = {
69 depth: item.depth,
70 /*
71 * Unknown until after traversal
72 */
73 isLastChild: false,
74 /*
75 * Unknown until after traversal
76 */
77 isLastSibling: false,
78 /*
79 * If it's a top level reply, bc we render each top-level branch as a
80 * separate tree, it's implicitly part of the last branch. For subsequent
81 * replies, we'll override this after traversal.
82 */
83 isPartOfLastBranchFromDepth: item.depth === 1 ? 1 : undefined,
84 nextItemDepth: nextItem?.depth,
85 parentMetadata,
86 prevItemDepth: prevItem?.depth,
87 /*
88 * Unknown until after traversal
89 */
90 precedesChildReadMore: false,
91 /*
92 * Unknown until after traversal
93 */
94 followsReadMoreUp: false,
95 postData: {
96 uri: item.uri,
97 authorHandle: item.value.post.author.handle,
98 },
99 repliesCount,
100 repliesUnhydrated,
101 repliesSeenCounter: 0,
102 replyIndex: 0,
103 skippedIndentIndices: new Set<number>(),
104 }
105
106 if (isDevMode()) {
107 // @ts-ignore dev only for debugging
108 metadata.postData.text = getPostRecord(item.value.post).text
109 }
110
111 return metadata
112}
113
114export function storeTraversalMetadata(
115 metadatas: Map<string, TraversalMetadata>,
116 metadata: TraversalMetadata,
117) {
118 metadatas.set(metadata.postData.uri, metadata)
119
120 if (isDevMode()) {
121 // @ts-ignore dev only for debugging
122 metadatas.set(metadata.postData.text, metadata)
123 // @ts-ignore
124 window.__thread = metadatas
125 }
126}
127
128export function getThreadPostUI({
129 depth,
130 repliesCount,
131 prevItemDepth,
132 isLastChild,
133 skippedIndentIndices,
134 repliesSeenCounter,
135 repliesUnhydrated,
136 precedesChildReadMore,
137 followsReadMoreUp,
138}: TraversalMetadata): Extract<ThreadItem, {type: 'threadPost'}>['ui'] {
139 const isReplyAndHasReplies =
140 depth > 0 &&
141 repliesCount > 0 &&
142 (repliesCount - repliesUnhydrated === repliesSeenCounter ||
143 repliesSeenCounter > 0)
144 return {
145 isAnchor: depth === 0,
146 showParentReplyLine:
147 followsReadMoreUp ||
148 (!!prevItemDepth && prevItemDepth !== 0 && prevItemDepth < depth),
149 showChildReplyLine: depth < 0 || isReplyAndHasReplies,
150 indent: depth,
151 /*
152 * If there are no slices below this one, or the next slice has a depth <=
153 * than the depth of this post, it's the last child of the reply tree. It
154 * is not necessarily the last leaf in the parent branch, since it could
155 * have another sibling.
156 */
157 isLastChild,
158 skippedIndentIndices,
159 precedesChildReadMore: precedesChildReadMore ?? false,
160 }
161}
162
163export function getThreadPostNoUnauthenticatedUI({
164 depth,
165 prevItemDepth,
166}: {
167 depth: number
168 prevItemDepth?: number
169 nextItemDepth?: number
170}): Extract<ThreadItem, {type: 'threadPostNoUnauthenticated'}>['ui'] {
171 return {
172 showChildReplyLine: depth < 0,
173 showParentReplyLine: Boolean(prevItemDepth && prevItemDepth < depth),
174 }
175}